@mwcp/otel
Version:
midway component for open telemetry
330 lines (277 loc) • 9.32 kB
text/typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
import assert from 'node:assert'
import {
App,
ApplicationContext,
IMidwayContainer,
Init,
Inject,
Singleton,
} from '@midwayjs/core'
import {
type ClzInstance,
type Context,
type DecoratorExecutorParamBase,
type GrpcContext,
Application,
MConfig,
retrieveRequestProtocolFromCtx,
} from '@mwcp/share'
import {
Attributes,
Context as TraceContext,
ROOT_CONTEXT,
Span,
SpanKind,
SpanOptions,
SpanStatusCode,
TimeInput,
propagation,
} from '@opentelemetry/api'
import { genISO8601String } from '@waiting/shared-core'
import type { MethodTypeUnknown } from '@waiting/shared-types'
import { OtelComponent } from '../component.js'
import { initSpanStatusOptions } from '../config.js'
import {
type Config,
type MiddlewareConfig,
type SpanStatusOptions,
AttrNames,
ConfigKey,
TraceScopeType,
middlewareEnableCacheKey,
} from '../types.js'
import {
genRequestSpanName,
getIncomingRequestAttributesFromWebContext,
getSpan,
setSpanWithRequestHeaders,
} from '../util.js'
import { TraceServiceSpan } from './trace.service.span.js'
import type { DecoratorTraceDataResp, DecoratorTraceDataRespAsync } from './trace.service.types.js'
export class TraceService extends TraceServiceSpan {
declare readonly app: Application
declare readonly applicationContext: IMidwayContainer
declare readonly config: Config
readonly mwConfig: MiddlewareConfig
declare readonly otel: OtelComponent
async init(): Promise<void> {
await this.startOnInit(this.app)
}
async startOnRequest(webCtx: Context): Promise<TraceContext | undefined> {
if (! this.config.enable) { return }
if (webCtx.getAttr(middlewareEnableCacheKey) !== 'true') { return }
if (this.isStartedMap.get(webCtx) === true) {
return this.getActiveContext()
}
await this.addRequestRouterInfo(webCtx)
const traceContext = this.initRootSpan(webCtx)
this.isStartedMap.set(webCtx, true)
const events: Attributes = {
event: AttrNames.RequestBegin,
time: this.startTime,
}
const rootSpan = getSpan(traceContext)
// const rootSpan = this.getRootSpan(webCtx)
// assert(rootSpan === rootSpan, 'span should be equal to rootSpan')
if (rootSpan) {
this.addEvent(rootSpan, events)
setSpanWithRequestHeaders(
rootSpan,
this.otel.captureHeadersMap.get('request'),
(key) => {
if (typeof webCtx.get === 'function') {
return webCtx.get(key)
}
},
)
}
Promise.resolve()
.then(async () => {
const attrs = await getIncomingRequestAttributesFromWebContext(webCtx, this.config)
attrs[AttrNames.RequestStartTime] = this.startTime
this.setAttributes(rootSpan, attrs)
})
.catch((err: Error) => {
this.setRootSpanWithError(err, void 0, webCtx)
console.error(err)
})
return traceContext
}
/**
* Finish the root span and clean the context.
*/
finish(
webCtx: Application | Context | GrpcContext,
spanStatusOptions: SpanStatusOptions = initSpanStatusOptions,
endTime?: TimeInput,
): void {
if (! this.config.enable) { return }
if (! this.isStartedMap.get(webCtx)) { return }
const time = genISO8601String()
const events: Attributes = {
time,
event: AttrNames.RequestEnd,
}
const rootSpan = this.getRootSpan(webCtx)
assert(rootSpan, 'rootSpan should not be null')
this.addEvent(rootSpan, events)
const attr: Attributes = {
[AttrNames.RequestEndTime]: time,
}
this.setAttributes(rootSpan, attr)
if (spanStatusOptions.code !== SpanStatusCode.ERROR) {
spanStatusOptions.code = SpanStatusCode.OK
}
this.endRootSpan(spanStatusOptions, endTime, webCtx)
this.delActiveContext(webCtx)
}
// #region protected methods
protected async startOnInit(webApplication: Application): Promise<void> {
if (! this.config.enable) { return }
if (this.isStartedMap.get(webApplication) === true) { return }
this.isStartedMap.set(webApplication, true)
// const events: Attributes = {
// event: AttrNames.RequestBegin,
// time: this.startTime,
// }
// const rootSpan = this.getRootSpan(webApplication)
// rootSpan && this.addEvent(rootSpan, events)
}
protected initRootSpan(scope: Context): TraceContext {
assert(scope, 'initRootSpan() webCtx should not be null, maybe this calling is not in a request context')
let ret: TraceContext | undefined = void 0
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const traceCtx = typeof scope.getApp === 'function' && scope.request?.headers
? propagation.extract(ROOT_CONTEXT, scope.request.headers)
: ROOT_CONTEXT
this.startActiveSpan(
this.genRootSpanName(scope),
(span, ctx) => {
assert(span, 'rootSpan should not be null on init')
this.setRootContext(scope, ctx)
ret = ctx
},
{ kind: SpanKind.SERVER },
traceCtx,
scope,
)
assert(ret, 'initRootSpan() failed')
return ret
}
protected genRootSpanName(scope: Context): string {
const routerInfo = this.getRequestRouterInfo(scope)
const protocol = retrieveRequestProtocolFromCtx(scope) || 'unknown'
const opts = {
protocol,
method: scope.method,
route: routerInfo?.fullUrl ?? scope.path,
}
const spanName = this.config.rootSpanName && typeof this.config.rootSpanName === 'function'
? this.config.rootSpanName(scope)
: genRequestSpanName(opts)
return spanName
}
}
// #region types
export interface GenDecoratorExecutorOptions {
config: Config
traceService: TraceService
}
export type ExecutorParamBase<T extends TraceDecoratorOptions = TraceDecoratorOptions> = DecoratorExecutorParamBase<T>
export type DecoratorExecutorParam<T extends TraceDecoratorOptions = TraceDecoratorOptions> = ExecutorParamBase<T>
& GenDecoratorExecutorOptions
& {
readonly rootTraceContext: TraceContext,
callerAttr: { [AttrNames.CallerClass]: string, [AttrNames.CallerMethod]: string },
spanName: string,
spanOptions: Partial<SpanOptions>,
startActiveSpan: boolean,
traceContext: TraceContext | undefined,
traceScope: TraceScopeType | undefined,
span: Span | undefined,
}
export type TraceOptions<M extends MethodTypeUnknown | undefined = undefined> = Partial<TraceDecoratorOptions<M>> | string
// #region TraceDecoratorOptions
export interface TraceDecoratorOptions<
/** Decorated method */
M extends MethodTypeUnknown | undefined = undefined,
/** Arguments of decorated method */
MParamType = M extends MethodTypeUnknown<infer P> ? P : unknown[],
MResultType = M extends MethodTypeUnknown<any[], infer R> ? R : unknown,
MThis = unknown extends ThisParameterType<M> ? ClzInstance : ThisParameterType<M>,
> extends SpanOptions {
/** @default `{target.name}/{methodName}` */
spanName: string | KeyGenerator<MThis, MParamType> | undefined
/**
* @default true
*/
startActiveSpan: boolean
traceContext: TraceContext | undefined
/**
* Used as the prefix of the span name,
* if spanName is not provided,
* and the Caller ClassName is `AutoConfiguration` | `ContainerConfiguration`,
* and the Caller MethodName is event name, such as `onReady` | `onServerReady`,
*/
namespace: string | undefined
/**
* @default `/`
*/
spanNameDelimiter: string | undefined
before: MethodTypeUnknown<
[MParamType, DecoratorContext<MThis>], // input args
DecoratorTraceDataResp | DecoratorTraceDataRespAsync, // output data
ThisParameterType<M> // this
> | undefined
after: MethodTypeUnknown<
[MParamType, Awaited<MResultType>, DecoratorContext<MThis>], // input args
DecoratorTraceDataResp | DecoratorTraceDataRespAsync, // output data
ThisParameterType<M> // this
> | undefined
afterThrow: MethodTypeUnknown<
[MParamType, Error, DecoratorContext<MThis>], // input args
DecoratorTraceDataResp | DecoratorTraceDataRespAsync, // output data
ThisParameterType<M> // this
> | undefined
/**
* @default true
*/
autoEndSpan: boolean | undefined
}
export type KeyGenerator<
TThis = any,
ArgsType = unknown[],
DContext extends DecoratorContext = DecoratorContext,
> = (
this: TThis,
/** Arguments of the method */
args: ArgsType,
context: DContext,
) => string | undefined
export type ScopeGenerator<
TThis = any,
ArgsType = unknown[],
DContext extends DecoratorContextBase = DecoratorContextBase,
> = (
this: TThis,
/** Arguments of the method */
args: ArgsType,
context: DContext,
) => object | symbol
export interface DecoratorContextBase {
webApp: Application | undefined
webContext: Context | undefined
traceService: TraceService | undefined
traceScope: TraceScopeType | undefined
/** Caller Class name */
instanceName: string
methodName: string
}
export interface DecoratorContext<T = ClzInstance> extends DecoratorContextBase {
traceContext: TraceContext | undefined
traceSpan: Span | undefined
instance: T
}