diff options
Diffstat (limited to 'u/trace')
-rw-r--r-- | u/trace/index.ts | 6 | ||||
-rw-r--r-- | u/trace/itrace.ts | 75 | ||||
-rw-r--r-- | u/trace/log/ansi.ts | 15 | ||||
-rw-r--r-- | u/trace/log/index.ts | 5 | ||||
-rw-r--r-- | u/trace/log/level.ts | 19 | ||||
-rw-r--r-- | u/trace/log/logger.ts | 5 | ||||
-rw-r--r-- | u/trace/log/pretty_json_console.ts | 39 | ||||
-rw-r--r-- | u/trace/log/trace.ts | 60 | ||||
-rw-r--r-- | u/trace/logger.ts | 126 | ||||
-rw-r--r-- | u/trace/metric/emittable.ts | 18 | ||||
-rw-r--r-- | u/trace/metric/index.ts | 41 | ||||
-rw-r--r-- | u/trace/metric/metric.ts | 54 | ||||
-rw-r--r-- | u/trace/metric/trace.ts | 59 | ||||
-rw-r--r-- | u/trace/metrics.ts | 140 | ||||
-rw-r--r-- | u/trace/trace.ts | 14 | ||||
-rw-r--r-- | u/trace/util.ts | 72 |
16 files changed, 417 insertions, 331 deletions
diff --git a/u/trace/index.ts b/u/trace/index.ts index 18da87a..332fb52 100644 --- a/u/trace/index.ts +++ b/u/trace/index.ts @@ -1,5 +1,5 @@ export * from './itrace.js'; -export * from './util.js'; -export * from './logger.js'; -export * from './metrics.js'; +export * from './metric/index.js'; +export * from './log/index.js'; export * from './trace.js'; +export * from './util.js'; diff --git a/u/trace/itrace.ts b/u/trace/itrace.ts index 8cf123a..9c33ad2 100644 --- a/u/trace/itrace.ts +++ b/u/trace/itrace.ts @@ -1,69 +1,90 @@ import type { Mapper, SideEffect, Supplier } from '@emprespresso/pengueno'; -// the "thing" every Trace writer must "trace()" +/** + * the "thing" every Trace writer must "trace()". + */ type BaseTraceWith = string; export type ITraceWith<T> = BaseTraceWith | T; export interface ITrace<TraceWith> { - addTrace: Mapper<ITraceWith<TraceWith>, ITrace<TraceWith>>; + /** + * creates a new trace scope which inherits from this trace. + */ + traceScope: Mapper<ITraceWith<TraceWith>, ITrace<TraceWith>>; + + /** + * does the tracing. + */ trace: SideEffect<ITraceWith<TraceWith>>; } -export type ITraceableTuple<T, TraceWith> = [T, BaseTraceWith | TraceWith]; -export type ITraceableMapper<T, _T, TraceWith, W = ITraceable<T, TraceWith>> = (w: W) => _T; +export type ITraceableTuple<T, TraceWith> = { item: T; trace: BaseTraceWith | TraceWith }; +export type ITraceableMapper<T, _T, TraceWith> = (w: ITraceable<T, TraceWith>) => _T; export interface ITraceable<T, Trace = BaseTraceWith> { readonly trace: ITrace<Trace>; - get: Supplier<T>; - move: <_T>(t: _T) => ITraceable<_T, Trace>; - map: <_T>(mapper: ITraceableMapper<T, _T, Trace>) => ITraceable<_T, Trace>; - bimap: <_T>(mapper: ITraceableMapper<T, ITraceableTuple<_T, Array<Trace> | Trace>, Trace>) => ITraceable<_T, Trace>; - peek: (peek: ITraceableMapper<T, void, Trace>) => ITraceable<T, Trace>; - flatMap: <_T>(mapper: ITraceableMapper<T, ITraceable<_T, Trace>, Trace>) => ITraceable<_T, Trace>; - flatMapAsync<_T>( + readonly get: Supplier<T>; + + readonly move: <_T>(t: _T) => ITraceable<_T, Trace>; + readonly map: <_T>(mapper: ITraceableMapper<T, _T, Trace>) => ITraceable<_T, Trace>; + readonly bimap: <_T>(mapper: ITraceableMapper<T, ITraceableTuple<_T, Trace>, Trace>) => ITraceable<_T, Trace>; + readonly coExtend: <_T>( + mapper: ITraceableMapper<T, ReadonlyArray<_T>, Trace>, + ) => ReadonlyArray<ITraceable<_T, Trace>>; + readonly peek: (peek: ITraceableMapper<T, void, Trace>) => ITraceable<T, Trace>; + + readonly traceScope: (mapper: ITraceableMapper<T, Trace, Trace>) => ITraceable<T, Trace>; + + readonly flatMap: <_T>(mapper: ITraceableMapper<T, ITraceable<_T, Trace>, Trace>) => ITraceable<_T, Trace>; + readonly flatMapAsync: <_T>( mapper: ITraceableMapper<T, Promise<ITraceable<_T, Trace>>, Trace>, - ): ITraceable<Promise<_T>, Trace>; + ) => ITraceable<Promise<_T>, Trace>; } -export class TraceableImpl<T, TraceWith> implements ITraceable<T, TraceWith> { +export class TraceableImpl<T, Trace> implements ITraceable<T, Trace> { protected constructor( private readonly item: T, - public readonly trace: ITrace<TraceWith>, + public readonly trace: ITrace<Trace>, ) {} - public map<_T>(mapper: ITraceableMapper<T, _T, TraceWith>) { + public map<_T>(mapper: ITraceableMapper<T, _T, Trace>) { const result = mapper(this); return new TraceableImpl(result, this.trace); } - public flatMap<_T>(mapper: ITraceableMapper<T, ITraceable<_T, TraceWith>, TraceWith>): ITraceable<_T, TraceWith> { + public coExtend<_T>(mapper: ITraceableMapper<T, ReadonlyArray<_T>, Trace>): ReadonlyArray<ITraceable<_T, Trace>> { + const results = mapper(this); + return Array.from(results).map((result) => this.move(result)); + } + + public flatMap<_T>(mapper: ITraceableMapper<T, ITraceable<_T, Trace>, Trace>): ITraceable<_T, Trace> { return mapper(this); } public flatMapAsync<_T>( - mapper: ITraceableMapper<T, Promise<ITraceable<_T, TraceWith>>, TraceWith>, - ): ITraceable<Promise<_T>, TraceWith> { + mapper: ITraceableMapper<T, Promise<ITraceable<_T, Trace>>, Trace>, + ): ITraceable<Promise<_T>, Trace> { return new TraceableImpl( mapper(this).then((t) => t.get()), this.trace, ); } - public peek(peek: ITraceableMapper<T, void, TraceWith>) { + public traceScope(mapper: ITraceableMapper<T, Trace, Trace>): ITraceable<T, Trace> { + return new TraceableImpl(this.get(), this.trace.traceScope(mapper(this))); + } + + public peek(peek: ITraceableMapper<T, void, Trace>) { peek(this); return this; } - public move<_T>(t: _T): ITraceable<_T, TraceWith> { + public move<_T>(t: _T): ITraceable<_T, Trace> { return this.map(() => t); } - public bimap<_T>(mapper: ITraceableMapper<T, ITraceableTuple<_T, Array<TraceWith> | TraceWith>, TraceWith>) { - const [item, trace] = mapper(this); - const traces = Array.isArray(trace) ? trace : [trace]; - return new TraceableImpl( - item, - traces.reduce((trace, _trace) => trace.addTrace(_trace), this.trace), - ); + public bimap<_T>(mapper: ITraceableMapper<T, ITraceableTuple<_T, Trace>, Trace>) { + const { item, trace: _trace } = mapper(this); + return this.move(item).traceScope(() => <Trace>_trace); } public get() { diff --git a/u/trace/log/ansi.ts b/u/trace/log/ansi.ts new file mode 100644 index 0000000..7ff16a3 --- /dev/null +++ b/u/trace/log/ansi.ts @@ -0,0 +1,15 @@ +export const ANSI = { + RESET: '\x1b[0m', + BOLD: '\x1b[1m', + DIM: '\x1b[2m', + RED: '\x1b[31m', + GREEN: '\x1b[32m', + YELLOW: '\x1b[33m', + BLUE: '\x1b[34m', + MAGENTA: '\x1b[35m', + CYAN: '\x1b[36m', + WHITE: '\x1b[37m', + BRIGHT_RED: '\x1b[91m', + BRIGHT_YELLOW: '\x1b[93m', + GRAY: '\x1b[90m', +}; diff --git a/u/trace/log/index.ts b/u/trace/log/index.ts new file mode 100644 index 0000000..670e333 --- /dev/null +++ b/u/trace/log/index.ts @@ -0,0 +1,5 @@ +export * from './ansi.js'; +export * from './level.js'; +export * from './logger.js'; +export * from './pretty_json_console.js'; +export * from './trace.js'; diff --git a/u/trace/log/level.ts b/u/trace/log/level.ts new file mode 100644 index 0000000..027dd71 --- /dev/null +++ b/u/trace/log/level.ts @@ -0,0 +1,19 @@ +export enum LogLevel { + UNKNOWN = 'UNKNOWN', + INFO = 'INFO', + WARN = 'WARN', + DEBUG = 'DEBUG', + ERROR = 'ERROR', + SYS = 'SYS', +} + +export const logLevelOrder: Array<LogLevel> = [ + LogLevel.DEBUG, + LogLevel.INFO, + LogLevel.WARN, + LogLevel.ERROR, + LogLevel.SYS, +]; + +export const isLogLevel = (l: unknown): l is LogLevel => + typeof l === 'string' && logLevelOrder.some((level) => level === l); diff --git a/u/trace/log/logger.ts b/u/trace/log/logger.ts new file mode 100644 index 0000000..3ced60a --- /dev/null +++ b/u/trace/log/logger.ts @@ -0,0 +1,5 @@ +import { LogLevel } from './level.js'; + +export interface ILogger { + readonly log: (level: LogLevel, ...args: string[]) => void; +} diff --git a/u/trace/log/pretty_json_console.ts b/u/trace/log/pretty_json_console.ts new file mode 100644 index 0000000..758af51 --- /dev/null +++ b/u/trace/log/pretty_json_console.ts @@ -0,0 +1,39 @@ +import { ANSI, LogLevel, ILogger } from './index.js'; + +export class PrettyJsonConsoleLogger implements ILogger { + public log(level: LogLevel, ...trace: string[]) { + const message = JSON.stringify( + { + level, + trace, + }, + null, + 4, + ); + const styled = `${this.getStyle(level)}${message}${ANSI.RESET}\n`; + this.getStream(level)(styled); + } + + private getStream(level: LogLevel) { + if (level === LogLevel.ERROR) { + return console.error; + } + return console.log; + } + + private getStyle(level: LogLevel) { + switch (level) { + case LogLevel.UNKNOWN: + case LogLevel.INFO: + return `${ANSI.MAGENTA}`; + case LogLevel.DEBUG: + return `${ANSI.CYAN}`; + case LogLevel.WARN: + return `${ANSI.BRIGHT_YELLOW}`; + case LogLevel.ERROR: + return `${ANSI.BRIGHT_RED}`; + case LogLevel.SYS: + return `${ANSI.DIM}${ANSI.BLUE}`; + } + } +} diff --git a/u/trace/log/trace.ts b/u/trace/log/trace.ts new file mode 100644 index 0000000..3f9f1b2 --- /dev/null +++ b/u/trace/log/trace.ts @@ -0,0 +1,60 @@ +import { isDebug, ITrace, ITraceWith, memoize, Supplier } from '@emprespresso/pengueno'; +import { ILogger, isLogLevel, LogLevel, logLevelOrder, PrettyJsonConsoleLogger } from './index.js'; + +export type LogTraceSupplier = ITraceWith<Supplier<string>> | ITraceWith<Error>; + +export class LogTrace implements ITrace<LogTraceSupplier> { + constructor( + private readonly logger: ILogger = new PrettyJsonConsoleLogger(), + private readonly traces: Array<LogTraceSupplier> = [defaultTrace], + private readonly defaultLevel: LogLevel = LogLevel.INFO, + private readonly allowedLevels: Supplier<Set<LogLevel>> = defaultAllowedLevelsSupplier, + ) {} + + public traceScope(trace: LogTraceSupplier): ITrace<LogTraceSupplier> { + return new LogTrace(this.logger, this.traces.concat(trace), this.defaultLevel, this.allowedLevels); + } + + public trace(trace: LogTraceSupplier) { + const { traces, level: _level } = this.foldTraces(this.traces.concat(trace)); + if (!this.allowedLevels().has(_level)) return; + + const level = _level === LogLevel.UNKNOWN ? this.defaultLevel : _level; + this.logger.log(level, ...traces); + } + + private foldTraces(_traces: Array<LogTraceSupplier>) { + const _logTraces = _traces.map((trace) => (typeof trace === 'function' ? trace() : trace)); + const _level = _logTraces + .filter((trace) => isLogLevel(trace)) + .reduce((acc, level) => Math.max(logLevelOrder.indexOf(level), acc), -1); + const level = logLevelOrder[_level] ?? LogLevel.UNKNOWN; + + const traces = _logTraces + .filter((trace) => !isLogLevel(trace)) + .map((trace) => { + if (typeof trace === 'object') { + return `TracedException.Name = ${trace.name}, TracedException.Message = ${trace.message}, TracedException.Stack = ${trace.stack}`; + } + return trace; + }); + return { + level, + traces, + }; + } +} + +const defaultTrace = () => `TimeStamp = ${new Date().toISOString()}`; +const defaultAllowedLevels = memoize( + (isDebug: boolean) => + new Set([ + LogLevel.UNKNOWN, + ...(isDebug ? [LogLevel.DEBUG] : []), + LogLevel.INFO, + LogLevel.WARN, + LogLevel.ERROR, + LogLevel.SYS, + ]), +); +const defaultAllowedLevelsSupplier = () => defaultAllowedLevels(isDebug()); diff --git a/u/trace/logger.ts b/u/trace/logger.ts deleted file mode 100644 index 91432fe..0000000 --- a/u/trace/logger.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { isDebug, type ITrace, type ITraceWith, type Supplier } from '@emprespresso/pengueno'; - -export type LogTraceSupplier = ITraceWith<Supplier<string> | Error>; -const defaultTrace = () => `TimeStamp = ${new Date().toISOString()}`; -export class LogTrace implements ITrace<LogTraceSupplier> { - constructor( - private readonly logger: ILogger = new LoggerImpl(), - private readonly traces: Array<LogTraceSupplier> = [defaultTrace], - private readonly defaultLevel: LogLevel = LogLevel.INFO, - private readonly allowedLevels: Supplier<Array<LogLevel>> = defaultAllowedLevels, - ) {} - - public addTrace(trace: LogTraceSupplier): ITrace<LogTraceSupplier> { - return new LogTrace(this.logger, this.traces.concat(trace), this.defaultLevel, this.allowedLevels); - } - - public trace(trace: LogTraceSupplier) { - const { traces, level: _level } = this.foldTraces(this.traces.concat(trace)); - if (!this.allowedLevels().includes(_level)) return; - - const level = _level === LogLevel.UNKNOWN ? this.defaultLevel : _level; - this.logger.log(level, ...traces); - } - - private foldTraces(_traces: Array<LogTraceSupplier>) { - const _logTraces = _traces.map((trace) => (typeof trace === 'function' ? trace() : trace)); - const _level = _logTraces - .filter((trace) => isLogLevel(trace)) - .reduce((acc, level) => Math.max(logLevelOrder.indexOf(level), acc), -1); - const level = logLevelOrder[_level] ?? LogLevel.UNKNOWN; - - const traces = _logTraces - .filter((trace) => !isLogLevel(trace)) - .map((trace) => { - if (typeof trace === 'object') { - return `TracedException.Name = ${trace.name}, TracedException.Message = ${trace.message}, TracedException.Stack = ${trace.stack}`; - } - return trace; - }); - return { - level, - traces, - }; - } -} - -export enum LogLevel { - UNKNOWN = 'UNKNOWN', - INFO = 'INFO', - WARN = 'WARN', - DEBUG = 'DEBUG', - ERROR = 'ERROR', - SYS = 'SYS', -} -const logLevelOrder: Array<LogLevel> = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR, LogLevel.SYS]; -export const isLogLevel = (l: unknown): l is LogLevel => - typeof l === 'string' && logLevelOrder.some((level) => level === l); - -const defaultAllowedLevels = () => - [ - LogLevel.UNKNOWN, - ...(isDebug() ? [LogLevel.DEBUG] : []), - LogLevel.INFO, - LogLevel.WARN, - LogLevel.ERROR, - LogLevel.SYS, - ] as Array<LogLevel>; - -export interface ILogger { - readonly log: (level: LogLevel, ...args: string[]) => void; -} -class LoggerImpl implements ILogger { - private readonly textEncoder = new TextEncoder(); - - public log(level: LogLevel, ...trace: string[]) { - const message = JSON.stringify( - { - level, - trace, - }, - null, - 4, - ); - const styled = `${this.getStyle(level)}${message}${ANSI.RESET}\n`; - this.getStream(level)(this.textEncoder.encode(styled)); - } - - private getStream(level: LogLevel) { - if (level === LogLevel.ERROR) { - return console.error; - } - return console.log; - } - - private getStyle(level: LogLevel) { - switch (level) { - case LogLevel.UNKNOWN: - case LogLevel.INFO: - return `${ANSI.MAGENTA}`; - case LogLevel.DEBUG: - return `${ANSI.CYAN}`; - case LogLevel.WARN: - return `${ANSI.BRIGHT_YELLOW}`; - case LogLevel.ERROR: - return `${ANSI.BRIGHT_RED}`; - case LogLevel.SYS: - return `${ANSI.DIM}${ANSI.BLUE}`; - } - } -} - -export const ANSI = { - RESET: '\x1b[0m', - BOLD: '\x1b[1m', - DIM: '\x1b[2m', - RED: '\x1b[31m', - GREEN: '\x1b[32m', - YELLOW: '\x1b[33m', - BLUE: '\x1b[34m', - MAGENTA: '\x1b[35m', - CYAN: '\x1b[36m', - WHITE: '\x1b[37m', - BRIGHT_RED: '\x1b[91m', - BRIGHT_YELLOW: '\x1b[93m', - GRAY: '\x1b[90m', -}; diff --git a/u/trace/metric/emittable.ts b/u/trace/metric/emittable.ts new file mode 100644 index 0000000..f3441ec --- /dev/null +++ b/u/trace/metric/emittable.ts @@ -0,0 +1,18 @@ +import { IEmittableMetric, MetricValue, MetricValueTag, Unit } from './index.js'; + +export class EmittableMetric implements IEmittableMetric { + constructor( + public readonly name: string, + public readonly unit: Unit, + ) {} + + public withValue(value: number): MetricValue { + return { + name: this.name, + unit: this.unit, + emissionTimestamp: Date.now(), + value, + _tag: MetricValueTag, + }; + } +} diff --git a/u/trace/metric/index.ts b/u/trace/metric/index.ts new file mode 100644 index 0000000..72c37d2 --- /dev/null +++ b/u/trace/metric/index.ts @@ -0,0 +1,41 @@ +import { isTagged, Tagged, type Mapper } from '@emprespresso/pengueno'; + +export enum Unit { + COUNT = 'COUNT', + MILLISECONDS = 'MILLISECONDS', +} + +export const MetricValueTag = 'MetricValue' as const; +export type MetricValueTag = typeof MetricValueTag; +export const isMetricValue = (t: unknown): t is MetricValue => isTagged(t, MetricValueTag); +export interface MetricValue extends Tagged<MetricValueTag> { + readonly name: string; + readonly unit: Unit; + readonly value: number; + readonly emissionTimestamp: number; +} + +export interface IEmittableMetric { + readonly name: string; + readonly unit: Unit; + readonly withValue: Mapper<number, MetricValue>; +} + +export const IMetricTag = 'IMetric' as const; +export type IMetricTag = typeof IMetricTag; +export const isIMetric = (t: unknown): t is IMetric => isTagged(t, IMetricTag); +export interface IMetric extends Tagged<IMetricTag> { + readonly count: IEmittableMetric; + readonly time: IEmittableMetric; + readonly parent: undefined | IMetric; +} + +export interface IResultMetric extends IMetric { + readonly failure: IMetric; + readonly success: IMetric; + readonly warn: IMetric; +} + +export * from './emittable.js'; +export * from './metric.js'; +export * from './trace.js'; diff --git a/u/trace/metric/metric.ts b/u/trace/metric/metric.ts new file mode 100644 index 0000000..8ef339f --- /dev/null +++ b/u/trace/metric/metric.ts @@ -0,0 +1,54 @@ +import { EmittableMetric, IMetric, IMetricTag, IResultMetric, Unit } from './index.js'; + +class _Tagged { + protected constructor(public readonly _tag = IMetricTag) {} +} + +export class Metric extends _Tagged implements IMetric { + private static DELIM = '.'; + + protected constructor( + public readonly name: string, + public readonly parent: undefined | IMetric = undefined, + public readonly count = new EmittableMetric(Metric.join(name, 'count'), Unit.COUNT), + public readonly time = new EmittableMetric(Metric.join(name, 'time'), Unit.MILLISECONDS), + ) { + super(); + } + + public child(_name: string): Metric { + const childName = Metric.join(this.name, _name); + return new Metric(childName, this); + } + + public asResult() { + return ResultMetric.from(this); + } + + static join(...name: Array<string>) { + return name.join(Metric.DELIM); + } + + static fromName(name: string): Metric { + return new Metric(name); + } +} + +export class ResultMetric extends Metric implements IResultMetric { + protected constructor( + public readonly name: string, + public readonly parent: undefined | IMetric = undefined, + public readonly failure: IMetric, + public readonly success: IMetric, + public readonly warn: IMetric, + ) { + super(name, parent); + } + + static from(metric: Metric) { + const failure = metric.child('failure'); + const success = metric.child('success'); + const warn = metric.child('warn'); + return new ResultMetric(metric.name, metric.parent, failure, success, warn); + } +} diff --git a/u/trace/metric/trace.ts b/u/trace/metric/trace.ts new file mode 100644 index 0000000..0c5fe37 --- /dev/null +++ b/u/trace/metric/trace.ts @@ -0,0 +1,59 @@ +import { IMetric, isIMetric, isMetricValue, ITrace, ITraceWith, MetricValue, SideEffect } from '@emprespresso/pengueno'; + +export type MetricsTraceSupplier = + | ITraceWith<IMetric | MetricValue | undefined> + | Array<ITraceWith<IMetric | MetricValue | undefined>>; +export const isMetricsTraceSupplier = (t: unknown): t is MetricsTraceSupplier => + isMetricValue(t) || isIMetric(t) || (Array.isArray(t) && t.every((_m) => isMetricValue(_m) || isIMetric(_m))); + +export class MetricsTrace implements ITrace<MetricsTraceSupplier> { + constructor( + private readonly metricConsumer: SideEffect<Array<MetricValue>>, + private readonly activeTraces: ReadonlyMap<IMetric, number> = new Map(), + private readonly completedTraces: ReadonlySet<IMetric> = new Set(), + ) {} + + public traceScope(trace: MetricsTraceSupplier): MetricsTrace { + const now = Date.now(); + const metricsToTrace = (Array.isArray(trace) ? trace : [trace]).filter(isIMetric); + + const initialTraces = new Map(metricsToTrace.map((metric) => [metric, now])); + + return new MetricsTrace(this.metricConsumer, initialTraces); + } + + public trace(metrics: MetricsTraceSupplier): MetricsTrace { + if (!metrics || typeof metrics === 'string') { + return this; + } + + const now = Date.now(); + const allMetrics = Array.isArray(metrics) ? metrics : [metrics]; + + // partition the incoming metrics + const valuesToEmit = allMetrics.filter(isMetricValue); + const traceableMetrics = allMetrics.filter(isIMetric); + + const metricsToStart = traceableMetrics.filter((m) => !this.activeTraces.has(m)); + const metricsToEnd = traceableMetrics.filter((m) => this.activeTraces.has(m) && !this.completedTraces.has(m)); + + // the new metrics to emit based on traces ending *now* + const endedMetricValues = metricsToEnd.flatMap((metric) => [ + metric.count.withValue(1.0), + metric.time.withValue(now - this.activeTraces.get(metric)!), + ]); + + const allMetricsToEmit = [...valuesToEmit, ...endedMetricValues]; + if (allMetricsToEmit.length > 0) { + this.metricConsumer(allMetricsToEmit); + } + + // the next immutable state + const nextActiveTraces = new Map([ + ...this.activeTraces, + ...metricsToStart.map((m): [IMetric, number] => [m, now]), + ]); + const nextCompletedTraces = new Set([...this.completedTraces, ...metricsToEnd]); + return new MetricsTrace(this.metricConsumer, nextActiveTraces, nextCompletedTraces); + } +} diff --git a/u/trace/metrics.ts b/u/trace/metrics.ts deleted file mode 100644 index 2301afd..0000000 --- a/u/trace/metrics.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { - isObject, - type ITrace, - type ITraceWith, - type Mapper, - type SideEffect, - type Supplier, -} from '@emprespresso/pengueno'; - -export enum Unit { - COUNT = 'COUNT', - MILLISECONDS = 'MILLISECONDS', -} - -export interface IMetric { - readonly count: IEmittableMetric; - readonly time: IEmittableMetric; - readonly failure: undefined | IMetric; - readonly success: undefined | IMetric; - readonly warn: undefined | IMetric; - readonly children: Supplier<Array<IMetric>>; - - readonly _tag: 'IMetric'; -} -export const isIMetric = (t: unknown): t is IMetric => isObject(t) && '_tag' in t && t._tag === 'IMetric'; - -export interface IEmittableMetric { - readonly name: string; - readonly unit: Unit; - withValue: Mapper<number, MetricValue>; -} - -export class EmittableMetric implements IEmittableMetric { - constructor( - public readonly name: string, - public readonly unit: Unit, - ) {} - - public withValue(value: number): MetricValue { - return { - name: this.name, - unit: this.unit, - emissionTimestamp: Date.now(), - value, - _tag: 'MetricValue', - }; - } -} - -export class Metric implements IMetric { - constructor( - public readonly count: IEmittableMetric, - public readonly time: IEmittableMetric, - public readonly failure: undefined | Metric = undefined, - public readonly success: undefined | Metric = undefined, - public readonly warn: undefined | Metric = undefined, - public readonly _tag: 'IMetric' = 'IMetric', - ) {} - - public children() { - return [this.failure, this.success, this.warn].filter((x) => x) as IMetric[]; - } - - static fromName(name: string, addChildren = true): Metric { - return new Metric( - new EmittableMetric(`${name}.count`, Unit.COUNT), - new EmittableMetric(`${name}.elapsed`, Unit.MILLISECONDS), - addChildren ? Metric.fromName(`${name}.failure`, false) : undefined, - addChildren ? Metric.fromName(`${name}.success`, false) : undefined, - addChildren ? Metric.fromName(`${name}.warn`, false) : undefined, - ); - } -} - -export interface MetricValue { - readonly name: string; - readonly unit: Unit; - readonly value: number; - readonly emissionTimestamp: number; - readonly _tag: 'MetricValue'; -} -export const isMetricValue = (t: unknown): t is MetricValue => isObject(t) && '_tag' in t && t._tag === 'MetricValue'; - -export const isMetricsTraceSupplier = (t: unknown): t is MetricsTraceSupplier => isMetricValue(t) || isIMetric(t); - -export type MetricsTraceSupplier = ITraceWith<IMetric | MetricValue | undefined>; -type MetricTracingTuple = [IMetric, Date]; -export class MetricsTrace implements ITrace<MetricsTraceSupplier> { - constructor( - private readonly metricConsumer: SideEffect<Array<MetricValue>>, - private readonly tracing: Array<MetricTracingTuple> = [], - private readonly flushed: Set<IMetric> = new Set(), - ) {} - - public addTrace(trace: MetricsTraceSupplier) { - if (!isIMetric(trace)) return this; - return new MetricsTrace(this.metricConsumer)._nowTracing(trace); - } - - public trace(metric: MetricsTraceSupplier) { - if (typeof metric === 'undefined' || typeof metric === 'string') return this; - if (isMetricValue(metric)) { - this.metricConsumer([metric]); - return this; - } - - const foundMetricValues = this.tracing - .flatMap(([tracing, startedTracing]) => - [tracing, ...tracing.children()] - .filter((_tracing) => metric === _tracing) - .flatMap((metric) => [ - this.addMetric(metric, startedTracing), - this.addMetric(tracing, startedTracing), - ]), - ) - .flatMap((values) => values); - - if (foundMetricValues.length === 0) { - return this._nowTracing(metric); - } - - this.metricConsumer(foundMetricValues); - return this; - } - - private addMetric(metric: IMetric, startedTracing: Date): Array<MetricValue> { - if (this.flushed.has(metric)) { - return []; - } - - this.flushed.add(metric); - return [metric.count.withValue(1.0), metric.time.withValue(Date.now() - startedTracing.getTime())]; - } - - private _nowTracing(metric?: IMetric): MetricsTrace { - if (!metric) return this; - this.tracing.push([metric, new Date()]); - return this; - } -} diff --git a/u/trace/trace.ts b/u/trace/trace.ts index acc116f..e316ca8 100644 --- a/u/trace/trace.ts +++ b/u/trace/trace.ts @@ -10,7 +10,7 @@ import { type MetricsTraceSupplier, type MetricValue, TraceableImpl, -} from '@emprespresso/pengueno'; +} from './index.js'; export class LogTraceable<T> extends TraceableImpl<T, LogTraceSupplier> { public static LogTrace = new LogTrace(); @@ -19,8 +19,10 @@ export class LogTraceable<T> extends TraceableImpl<T, LogTraceSupplier> { } } -const getEmbeddedMetricConsumer = (logTrace: ITrace<LogTraceSupplier>) => (metrics: Array<MetricValue>) => - logTrace.addTrace(LogLevel.SYS).trace(`Metrics = <metrics>${JSON.stringify(metrics)}</metrics>`); +const getEmbeddedMetricConsumer = (logTrace: ITrace<LogTraceSupplier>) => (metrics: Array<MetricValue>) => { + if (metrics.length === 0) return; + logTrace.traceScope(LogLevel.SYS).trace(`Metrics = <metrics>${JSON.stringify(metrics)}</metrics>`); +}; export class EmbeddedMetricsTraceable<T> extends TraceableImpl<T, MetricsTraceSupplier> { public static MetricsTrace = new MetricsTrace(getEmbeddedMetricConsumer(LogTraceable.LogTrace)); @@ -37,12 +39,12 @@ export class LogMetricTrace implements ITrace<LogMetricTraceSupplier> { private metricsTrace: ITrace<MetricsTraceSupplier>, ) {} - public addTrace(trace: LogTraceSupplier | MetricsTraceSupplier): LogMetricTrace { + public traceScope(trace: LogTraceSupplier | MetricsTraceSupplier): LogMetricTrace { if (isMetricsTraceSupplier(trace)) { - this.metricsTrace = this.metricsTrace.addTrace(trace); + this.metricsTrace = this.metricsTrace.traceScope(trace); return this; } - this.logTrace = this.logTrace.addTrace(trace); + this.logTrace = this.logTrace.traceScope(trace); return this; } diff --git a/u/trace/util.ts b/u/trace/util.ts index db1db63..ec67571 100644 --- a/u/trace/util.ts +++ b/u/trace/util.ts @@ -1,45 +1,59 @@ import { - ANSI, + IEither, + IMetric, + isEither, + ITraceable, + ITraceWith, + LogLevel, + ResultMetric, type Callable, - type IMetric, type ITraceableMapper, - type ITraceableTuple, - type MetricsTraceSupplier, } from '@emprespresso/pengueno'; export class TraceUtil { - static withTrace<T, Trace>( - trace: string, - ansi?: Array<keyof typeof ANSI>, - ): ITraceableMapper<T, ITraceableTuple<T, Trace | Array<Trace>>, Trace> { - if (ansi) { - return (t) => [t.get(), `${ansi.join('')}${trace}${ANSI.RESET}`]; - } - return (t) => [t.get(), trace]; + static promiseify<T, U, Trace>( + mapper: ITraceableMapper<T, U, Trace>, + ): ITraceableMapper<Promise<T>, Promise<U>, Trace> { + return (traceablePromise) => + traceablePromise.flatMapAsync(async (t) => t.move(await t.get()).map(mapper)).get(); } - static withMetricTrace<T, Trace extends MetricsTraceSupplier>( - metric: IMetric, - ): ITraceableMapper<T, ITraceableTuple<T, Trace | Array<Trace>>, Trace> { - return (t) => [t.get(), metric as Trace]; + static traceResultingEither<TErr, TOk, Trace>( + metric?: ResultMetric, + warnOnFailure = false, + ): ITraceableMapper<IEither<TErr, TOk>, ITraceable<IEither<TErr, TOk>, Trace>, Trace> { + return (t) => { + if (metric) + t.trace.trace( + t.get().fold( + (_err) => <Trace>(warnOnFailure ? metric.warn : metric.failure), + (_ok) => <Trace>metric.success, + ), + ); + return t.traceScope((_t) => + _t.get().fold( + (_err) => <Trace>(warnOnFailure ? LogLevel.WARN : LogLevel.ERROR), + (_ok) => <Trace>LogLevel.INFO, + ), + ); + }; } - static withFunctionTrace<F extends Callable, T, Trace>( - f: F, - ): ITraceableMapper<T, ITraceableTuple<T, Trace | Array<Trace>>, Trace> { - return TraceUtil.withTrace(`fn.${f.name}`); + static withTrace<T, Trace, _Trace extends ITraceWith<Trace>>( + trace: _Trace, + ): ITraceableMapper<T, ITraceable<T, Trace>, Trace> { + return (t) => t.traceScope(() => <Trace>trace); } - static withClassTrace<C extends object, T, Trace>( - c: C, - ): ITraceableMapper<T, ITraceableTuple<T, Trace | Array<Trace>>, Trace> { - return TraceUtil.withTrace(`class.${c.constructor.name}`); + static withMetricTrace<T, Trace>(metric: IMetric): ITraceableMapper<T, ITraceable<T, Trace>, Trace> { + return TraceUtil.withTrace(<Trace>metric); } - static promiseify<T, U, Trace>( - mapper: ITraceableMapper<T, U, Trace>, - ): ITraceableMapper<Promise<T>, Promise<U>, Trace> { - return (traceablePromise) => - traceablePromise.flatMapAsync(async (t) => t.move(await t.get()).map(mapper)).get(); + static withFunctionTrace<F extends Callable, T, Trace>(f: F): ITraceableMapper<T, ITraceable<T, Trace>, Trace> { + return TraceUtil.withTrace(<Trace>`fn.${f.name}`); + } + + static withClassTrace<C extends object, T, Trace>(c: C): ITraceableMapper<T, ITraceable<T, Trace>, Trace> { + return TraceUtil.withTrace(<Trace>`class.${c.constructor.name}`); } } |