Files
huly-platform/packages/measurements/src/context.ts
T
2025-07-31 22:58:25 +05:00

387 lines
10 KiB
TypeScript

// Basic performance metrics suite.
import { platformNow, platformNowDiff } from '.'
import { childMetrics, newMetrics, updateMeasure } from './metrics'
import {
type FullParamsType,
type MeasureContext,
type MeasureLogger,
type Metrics,
type ParamsType,
type OperationLog,
type OperationLogEntry,
type WithOptions
} from './types'
const errorPrinter = ({ message, stack, ...rest }: Error): object => ({
message,
stack,
...rest
})
function replacer (value: any): any {
return value instanceof Error ? errorPrinter(value) : value
}
export const consoleLogger = (logParams: Record<string, any>): MeasureLogger => ({
info: (msg, args) => {
console.info(
msg,
...Object.entries({ ...(args ?? {}), ...(logParams ?? {}) }).map(
(it) => `${it[0]}=${JSON.stringify(replacer(it[1]))}`
)
)
},
error: (msg, args) => {
console.error(
msg,
...Object.entries({ ...(args ?? {}), ...(logParams ?? {}) }).map(
(it) => `${it[0]}=${JSON.stringify(replacer(it[1]))}`
)
)
},
warn: (msg, args) => {
console.warn(msg, ...Object.entries(args ?? {}).map((it) => `${it[0]}=${JSON.stringify(replacer(it[1]))}`))
},
close: async () => {},
logOperation: (operation, time, params) => {}
})
export const noParamsLogger = consoleLogger({})
export const nullPromise = Promise.resolve()
/**
* @public
*/
export class MeasureMetricsContext implements MeasureContext {
private readonly name: string
private readonly params: ParamsType
private readonly fullParams: FullParamsType | (() => FullParamsType) = {}
logger: MeasureLogger
metrics: Metrics
id?: string
st = platformNow()
contextData: object = {}
private done (value?: number, override?: boolean): void {
updateMeasure(this.metrics, this.st, this.params, this.fullParams, (spend) => {}, value, override)
}
constructor (
name: string,
params: ParamsType,
fullParams: FullParamsType | (() => FullParamsType) = {},
metrics: Metrics = newMetrics(),
logger?: MeasureLogger,
readonly parent?: MeasureContext,
readonly logParams?: ParamsType
) {
this.name = name
this.params = params
this.fullParams = fullParams
this.metrics = metrics
this.metrics.namedParams = this.metrics.namedParams ?? {}
for (const [k, v] of Object.entries(params)) {
if (this.metrics.namedParams[k] !== v) {
this.metrics.namedParams[k] = v
} else {
this.metrics.namedParams[k] = '*'
}
}
this.logger = logger ?? (this.logParams != null ? consoleLogger(this.logParams ?? {}) : noParamsLogger)
}
measure (name: string, value: number, override?: boolean): void {
const c = new MeasureMetricsContext('#' + name, {}, {}, childMetrics(this.metrics, ['#' + name]), this.logger, this)
c.contextData = this.contextData
c.done(value, override)
}
newChild (
name: string,
params: ParamsType,
opt?: {
fullParams?: FullParamsType
logger?: MeasureLogger
span?: WithOptions['span'] // By default true
}
): MeasureContext {
const result = new MeasureMetricsContext(
name,
params,
opt?.fullParams ?? {},
childMetrics(this.metrics, [name]),
opt?.logger ?? this.logger,
this,
this.logParams
)
result.id = this.id
result.contextData = this.contextData
return result
}
with<T>(
name: string,
params: ParamsType,
op: (ctx: MeasureContext) => T | Promise<T>,
fullParams?: ParamsType | (() => FullParamsType),
opt?: WithOptions
): Promise<T> {
const c = this.newChild(name, params, { fullParams, logger: this.logger })
let needFinally = true
try {
const value = op(c)
if (value instanceof Promise) {
needFinally = false
return value.finally(() => {
c.end()
if (opt?.log === true) {
this.logger.logOperation(name, platformNowDiff((c as MeasureMetricsContext).st), {
...params,
...fullParams
})
}
})
} else {
if (value == null) {
return nullPromise as Promise<T>
}
return Promise.resolve(value)
}
} finally {
if (needFinally) {
c.end()
}
}
}
extractMeta (): Record<string, string | number | boolean> {
return {}
}
withSync<T>(
name: string,
params: ParamsType,
op: (ctx: MeasureContext) => T,
fullParams?: ParamsType | (() => FullParamsType)
): T {
const c = this.newChild(name, params, { fullParams, logger: this.logger })
try {
return op(c)
} finally {
c.end()
}
}
error (message: string, args?: Record<string, any>): void {
this.logger.error(message, { ...this.params, ...args, ...(this.logParams ?? {}) })
}
info (message: string, args?: Record<string, any>): void {
this.logger.info(message, { ...this.params, ...args, ...(this.logParams ?? {}) })
}
warn (message: string, args?: Record<string, any>): void {
this.logger.warn(message, { ...this.params, ...args, ...(this.logParams ?? {}) })
}
end (): void {
this.done()
}
getParams (): ParamsType {
return this.params
}
}
export class NoMetricsContext implements MeasureContext {
logger: MeasureLogger
id?: string
contextData: object = {}
constructor (logger?: MeasureLogger) {
this.logger = logger ?? consoleLogger({})
}
measure (name: string, value: number, override?: boolean): void {}
newChild (
name: string,
params: ParamsType,
fullParams?: FullParamsType | (() => FullParamsType),
logger?: MeasureLogger
): MeasureContext {
const result = new NoMetricsContext(logger ?? this.logger)
result.id = this.id
result.contextData = this.contextData
return result
}
with<T>(
name: string,
params: ParamsType,
op: (ctx: MeasureContext) => T | Promise<T>,
fullParams?: ParamsType | (() => FullParamsType)
): Promise<T> {
const r = op(this.newChild(name, params, fullParams, this.logger))
return r instanceof Promise ? r : Promise.resolve(r)
}
extractMeta (): Record<string, string | number | boolean> {
return {}
}
withSync<T>(
name: string,
params: ParamsType,
op: (ctx: MeasureContext) => T,
fullParams?: ParamsType | (() => FullParamsType)
): T {
const c = this.newChild(name, params, fullParams, this.logger)
return op(c)
}
withLog<T>(
name: string,
params: ParamsType,
op: (ctx: MeasureContext) => T | Promise<T>,
fullParams?: ParamsType
): Promise<T> {
const r = op(this.newChild(name, params, fullParams, this.logger))
return r instanceof Promise ? r : Promise.resolve(r)
}
error (message: string, args?: Record<string, any>): void {
this.logger.error(message, { ...args })
}
info (message: string, args?: Record<string, any>): void {
this.logger.info(message, { ...args })
}
warn (message: string, args?: Record<string, any>): void {
this.logger.warn(message, { ...args })
}
end (): void {}
getParams (): ParamsType {
return {}
}
}
/**
* Allow to use decorator for context enabled functions
*/
export function withContext (name: string, params: ParamsType = {}): any {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor => {
const originalMethod = descriptor.value
descriptor.value = function (...args: any[]): Promise<any> {
const ctx = args[0] as MeasureContext
return ctx.with(name, params, (ctx) => originalMethod.apply(this, [ctx, ...args.slice(1)]) as Promise<any>)
}
return descriptor
}
}
let operationProfiling = false
export function setOperationLogProfiling (value: boolean): void {
operationProfiling = value
}
let globalId: number = 0
export function registerOperationLog (ctx: MeasureContext): { opLogMetrics?: Metrics, op?: OperationLog } {
if (!operationProfiling) {
return {}
}
const op: OperationLog = { start: platformNow(), ops: [], end: -1 }
let opLogMetrics: Metrics | undefined
if (ctx.id === undefined) {
ctx.id = 'op_' + (++globalId).toString(16)
}
if (ctx.metrics !== undefined) {
if (ctx.metrics.opLog === undefined) {
ctx.metrics.opLog = {}
}
ctx.metrics.opLog[ctx.id] = op
opLogMetrics = ctx.metrics
}
return { opLogMetrics, op }
}
export function updateOperationLog (opLogMetrics: Metrics | undefined, op: OperationLog | undefined): void {
if (!operationProfiling) {
return
}
if (op !== undefined) {
op.end = platformNow()
}
// We should keep only longest one entry
if (opLogMetrics?.opLog !== undefined) {
const entries = Object.entries(opLogMetrics.opLog)
const incomplete = entries.filter((it) => it[1].end === -1)
const complete = entries.filter((it) => it[1].end !== -1)
complete.sort((a, b) => a[1].start - b[1].start)
if (complete.length > 30) {
complete.splice(0, complete.length - 30)
}
opLogMetrics.opLog = Object.fromEntries(incomplete.concat(complete))
}
}
export function addOperation<T> (
ctx: MeasureContext,
name: string,
params: ParamsType,
op: (ctx: MeasureContext) => Promise<T>,
fullParams?: FullParamsType
): Promise<T> {
if (!operationProfiling) {
return op(ctx)
}
let opEntry: OperationLogEntry | undefined
let p: MeasureContext | undefined = ctx
let opLogMetrics: Metrics | undefined
let id: string | undefined
while (p !== undefined) {
if (p.metrics?.opLog !== undefined) {
opLogMetrics = p.metrics
}
if (id === undefined && p.id !== undefined) {
id = p.id
}
p = p.parent
}
const opLog = id !== undefined ? opLogMetrics?.opLog?.[id] : undefined
if (opLog !== undefined) {
opEntry = {
op: name,
start: performance.now(),
params: {},
end: -1
}
}
const result = op(ctx)
if (opEntry !== undefined && opLog !== undefined) {
void result.finally(() => {
if (opEntry !== undefined && opLog !== undefined) {
opEntry.end = performance.now()
opEntry.params = { ...params, ...(typeof fullParams === 'function' ? fullParams() : fullParams) }
opLog.ops.push(opEntry)
}
})
}
return result
}