@mwcp/kmore
Version:
midway component for knex, supports declarative transaction and OpenTelemetry
372 lines (311 loc) • 11.8 kB
text/typescript
import assert from 'node:assert'
import {
App,
ApplicationContext,
IMidwayContainer,
Inject,
Singleton,
} from '@midwayjs/core'
import {
type Span,
type TraceService,
AttrNames,
Attributes,
DecoratorTraceData,
SpanStatusCode,
StartScopeActiveSpanOptions,
Trace,
TraceLog,
TraceScopeType,
getSpan,
} from '@mwcp/otel'
import { Application, Context, MConfig, getWebContext } from '@mwcp/share'
import { humanMemoryUsage } from '@waiting/shared-core'
import type { Kmore, KmoreEvent, KmoreQueryBuilder } from 'kmore'
import { eventNeedTrace, genCommonAttr } from './trace.helper.js'
import { TrxStatusService } from './trx-status.service.js'
import { ConfigKey, ConnectionConfig, DbConfig, KmoreAttrNames, KmoreSourceConfig } from './types.js'
export class DbEvent<SourceName extends string = string> {
private readonly sourceConfig: KmoreSourceConfig<SourceName>
readonly app: Application
readonly applicationContext: IMidwayContainer
readonly appDir: string
readonly baseDir: string
readonly trxStatusSvc: TrxStatusService
readonly traceService: TraceService
getDbConfigByDbId(dbId: SourceName): DbConfig | undefined {
assert(dbId)
const dbConfig = this.sourceConfig.dataSource[dbId]
return dbConfig
}
getWebContext(): Context | undefined {
return getWebContext(this.applicationContext)
}
getWebContextThenApp(): Context | Application {
try {
const webContext = getWebContext(this.applicationContext)
assert(webContext, 'getActiveContext() webContext should not be null, maybe this calling is not in a request context')
return webContext
}
catch (ex) {
console.warn('getWebContextThenApp() failed', ex)
return this.app
}
}
// #region onStart
<DbEvent['onStart']>({
autoEndSpan: false,
spanName: ([options]) => {
const { kmore, event } = options
// @ts-expect-error builder._method
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
let method = event.command?.toLowerCase() ?? event.queryBuilder._method ?? 'unknown'
if (method === 'del') {
method = 'delete'
}
const name = `Kmore ${kmore.dbId} ${method}`
return name
},
before([options], decoratorContext) {
if (! eventNeedTrace(KmoreAttrNames.BuilderCompile, options.dbConfig)) { return }
const traceContext = decoratorContext.traceContext ?? this.traceService.getActiveContext()
const { kmore, event } = options
const { kmoreQueryId, queryBuilder } = event
const traceScope = this.retrieveTraceScope(kmore, kmoreQueryId, queryBuilder)
if (! this.trxStatusSvc.getTraceContextByScope(traceScope)) {
this.trxStatusSvc.setTraceContextByScope(traceScope, traceContext)
}
this.trxStatusSvc.setTraceContextByScope(kmoreQueryId, traceContext)
const { pagingType } = queryBuilder
const traceSpan = getSpan(traceContext)
const ret: DecoratorTraceData = {}
if (pagingType && traceSpan) {
if (pagingType === 'counter') {
assert(queryBuilder.pagingGroupKey, 'queryBuilder.pagingGroupKey is empty')
if (! this.trxStatusSvc.getTraceContextByScope(queryBuilder.pagingGroupKey)) {
this.trxStatusSvc.setTraceContextByScope(queryBuilder.pagingGroupKey, traceContext)
}
// @ts-expect-error name
const spanName = traceSpan.name as string
const spanName2 = `${spanName} AutoPaging`
if (! spanName.endsWith('AutoPaging')) {
traceSpan.updateName(spanName2)
}
const opts: StartScopeActiveSpanOptions = {
name: 'Kmore Counter',
scope: traceScope,
traceContext,
}
const { traceContext: traceCtx2 } = this.traceService.startScopeSpan(opts)
this.trxStatusSvc.setTraceContextByScope(kmoreQueryId, traceCtx2)
ret.traceContext = traceCtx2 // necessary
}
else {
const spanName2 = 'Kmore Pager'
traceSpan.updateName(spanName2)
}
}
const events = genCommonAttr(KmoreAttrNames.BuilderCompile)
ret.events = events
return ret
},
})
onStart(this: DbEvent, options: OnEventOptions): void {
const { dbConfig, event, kmore } = options
assert(dbConfig)
assert(event.type === 'start', event.type)
assert(event.queryBuilder)
// if (event.queryBuilder) {
// void Object.defineProperty(event.queryBuilder, 'eventProcessed', {
// value: true,
// })
// }
const cb = dbConfig.eventCallbacks?.start
return cb?.(event, kmore)
}
// #region onResp
<DbEvent['onResp']>({
before([options]) {
if (! eventNeedTrace(KmoreAttrNames.QueryResponse, options.dbConfig)) { return }
const { respRaw } = options.event
const attrs: Attributes = {}
if (respRaw?.response?.command) {
attrs['db.operation'] = respRaw.response.command
attrs[AttrNames.QueryRowCount] = respRaw.response.rowCount ?? 0
// attrs[AttrNames.QueryResponse] = JSON.stringify(respRaw.response.rows, null, 2)
}
const events = genCommonAttr(KmoreAttrNames.QueryResponse)
if (respRaw?.response) {
events[AttrNames.QueryRowCount] = respRaw.response.rowCount ?? 0
events[AttrNames.QueryResponse] = JSON.stringify(respRaw.response.rows, null, 2)
}
return { attrs, events }
},
after([options]) {
if (! eventNeedTrace(KmoreAttrNames.QueryResponse, options.dbConfig)) { return }
const ret: DecoratorTraceData = {}
const { pagingType } = options.event.queryBuilder
const { kmore, event } = options
const { kmoreQueryId } = event
const traceScope = this.retrieveTraceScope(kmore, kmoreQueryId, event.queryBuilder)
switch (pagingType) {
case 'counter': {
this.trxStatusSvc.removeTraceContextByScope(kmoreQueryId)
ret.endSpanAfterTraceLog = true
break
}
case 'pager': {
const spans: Span[] = []
const scopeCtx = this.trxStatusSvc.getTraceContextByScope(traceScope)
if (scopeCtx) {
const span = getSpan(scopeCtx)
if (span?.isRecording()) {
spans.push(span)
}
}
const activeCtx = this.trxStatusSvc.getTraceContextByScope(event.kmoreQueryId)
if (activeCtx) {
const span = getSpan(activeCtx)
if (span?.isRecording()) {
spans.push(span)
}
}
this.trxStatusSvc.removeTraceContextByScope(kmoreQueryId)
const { pagingGroupKey } = event.queryBuilder
if (pagingGroupKey) {
this.trxStatusSvc.removeTraceContextByScope(pagingGroupKey)
}
this.trxStatusSvc.removeTraceContextByScope(traceScope)
ret.endSpanAfterTraceLog = spans
break
}
default: {
ret.endSpanAfterTraceLog = true
}
}
return ret
},
})
onResp(this: DbEvent, options: OnEventOptions): void {
const { dbConfig, event, kmore } = options
assert(dbConfig)
assert(event.type === 'queryResponse', event.type)
assert(event.respRaw)
const cb = dbConfig.eventCallbacks?.queryResponse
return cb?.(event, kmore)
}
// #region onQuery
<DbEvent['onQuery']>({
before([options], decoratorContext) {
if (! eventNeedTrace(KmoreAttrNames.QueryQuerying, options.dbConfig)) { return }
const { traceService, traceSpan } = decoratorContext
assert(traceService, 'traceService is empty')
assert(traceSpan, 'traceSpan is empty')
const { config: knexConfig } = options.dbConfig
const conn = knexConfig.connection as ConnectionConfig
const { dbId, kUid, queryUid, trxId, data } = options.event
const attrs: Attributes = {
dbId,
['db.system']: typeof knexConfig.client === 'string'
? knexConfig.client
: JSON.stringify(knexConfig.client, null, 2),
['net.peer.name']: conn.host,
['db.name']: conn.database,
['net.peer.port']: conn.port ?? '',
['db.user']: conn.user,
// [AttrNames.QueryCostThrottleInMS]: sampleThrottleMs,
kUid,
queryUid,
trxId: trxId ?? '',
['db.statement']: data?.sql ?? '',
}
const bindings = data?.bindings?.length ? JSON.stringify(data.bindings, null, 2) : void 0
const events = genCommonAttr(KmoreAttrNames.QueryQuerying, { bindings, kUid })
return { attrs, events }
},
})
onQuery(this: DbEvent, options: OnEventOptions): void {
const { dbConfig, event, kmore } = options
assert(dbConfig)
assert(event.type === 'query', event.type)
assert(event.data)
const cb = dbConfig.eventCallbacks?.query
return cb?.(event, kmore)
}
// #region onError
<DbEvent['onError']>({
before([options]) {
if (! eventNeedTrace(KmoreAttrNames.QueryError, options.dbConfig)) { return }
// if (! decoratorContext.traceScope) {
// const { kmore } = options
// const { kmoreQueryId, queryBuilder } = options.event
// decoratorContext.traceScope = this.retrieveTraceScope(kmore, kmoreQueryId, queryBuilder)
// }
const {
dbId, kUid, queryUid, trxId, exData, exError,
} = options.event
const events = genCommonAttr(KmoreAttrNames.QueryError, {
level: 'error',
dbId,
kUid,
queryUid,
trxId,
[AttrNames.ServiceMemoryUsage]: JSON.stringify(humanMemoryUsage(), null, 2),
exData: JSON.stringify(exData, null, 2),
exError: JSON.stringify(exError, null, 2),
})
const attrs: Attributes = {
[AttrNames.LogLevel]: 'error',
}
return { attrs, events }
},
after([options], _result, decoratorContext) {
if (! eventNeedTrace(KmoreAttrNames.QueryError, options.dbConfig)) { return }
const { traceService, traceScope, traceSpan } = decoratorContext
assert(traceService, 'traceService is empty')
// assert(traceScope, 'onResp.after() traceScope is empty')
assert(traceSpan, 'traceSpan is empty')
if (traceScope) {
const scopeRootSpan = traceService.getRootSpan(traceScope)
if (scopeRootSpan && scopeRootSpan === traceSpan) {
const spanStatusOptions = { code: SpanStatusCode.ERROR }
return { endSpanAfterTraceLog: true, spanStatusOptions }
}
}
return null
},
})
async onError(this: DbEvent, options: OnEventOptions): Promise<void> {
const { dbConfig, event, kmore } = options
assert(dbConfig)
assert(event.type === 'queryError', event.type)
const cb = dbConfig.eventCallbacks?.queryError
return cb?.(event, kmore)
}
retrieveTraceScope(kmore: Kmore, kmoreQueryId: symbol, builder: KmoreQueryBuilder): TraceScopeType {
const { pagingGroupKey, pagingType } = builder
const traceScope = this.getTrxTraceScopeByQueryId(kmore, kmoreQueryId)
if (pagingType) { // paging
if (pagingType === 'counter') { // counter
if (traceScope) {
return traceScope
}
}
// pager
return pagingGroupKey ?? kmoreQueryId
}
return traceScope ?? kmoreQueryId
}
protected getTrxTraceScopeByQueryId(db: Kmore, queryId: symbol): TraceScopeType | undefined {
const trx = db.getTrxByQueryId(queryId)
return trx?.kmoreTrxId
}
}
// #region types
interface OnEventOptions {
dataSourceName: string
dbConfig: DbConfig
event: KmoreEvent
kmore: Kmore
}