diff options
Diffstat (limited to 'u/trace')
-rw-r--r-- | u/trace/itrace.ts | 29 | ||||
-rw-r--r-- | u/trace/logger.ts | 87 | ||||
-rw-r--r-- | u/trace/metrics.ts | 133 | ||||
-rw-r--r-- | u/trace/mod.ts | 2 | ||||
-rw-r--r-- | u/trace/trace.ts | 94 | ||||
-rw-r--r-- | u/trace/util.ts | 48 |
6 files changed, 316 insertions, 77 deletions
diff --git a/u/trace/itrace.ts b/u/trace/itrace.ts index b9b750d..620fff0 100644 --- a/u/trace/itrace.ts +++ b/u/trace/itrace.ts @@ -1,4 +1,4 @@ -import type { Mapper, SideEffect } from "@emprespresso/pengueno"; +import type { Mapper, SideEffect, Supplier } from "@emprespresso/pengueno"; // the "thing" every Trace writer must "trace()" type BaseTraceWith = string; @@ -19,15 +19,18 @@ export type ITraceableMapper< ) => U; export interface ITraceable<T, Trace = BaseTraceWith> { - readonly item: T; readonly trace: ITrace<Trace>; - + get: Supplier<T>; move<U>(u: U): ITraceable<U, Trace>; map: <U>( mapper: ITraceableMapper<T, U, Trace>, ) => ITraceable<U, Trace>; bimap: <U>( - mapper: ITraceableMapper<T, ITraceableTuple<U, Trace>, Trace>, + mapper: ITraceableMapper< + T, + ITraceableTuple<U, Array<Trace> | Trace>, + Trace + >, ) => ITraceable<U, Trace>; peek: (peek: ITraceableMapper<T, void, Trace>) => ITraceable<T, Trace>; flatMap: <U>( @@ -40,8 +43,8 @@ export interface ITraceable<T, Trace = BaseTraceWith> { export class TraceableImpl<T, TraceWith> implements ITraceable<T, TraceWith> { protected constructor( - readonly item: T, - readonly trace: ITrace<TraceWith>, + private readonly item: T, + public readonly trace: ITrace<TraceWith>, ) {} public map<U>( @@ -69,7 +72,7 @@ export class TraceableImpl<T, TraceWith> implements ITraceable<T, TraceWith> { >, ): ITraceable<Promise<U>, TraceWith> { return new TraceableImpl( - mapper(this).then(({ item }) => item), + mapper(this).then((t) => t.get()), this.trace, ); } @@ -86,11 +89,19 @@ export class TraceableImpl<T, TraceWith> implements ITraceable<T, TraceWith> { public bimap<U>( mapper: ITraceableMapper< T, - ITraceableTuple<U, TraceWith>, + ITraceableTuple<U, Array<TraceWith> | TraceWith>, TraceWith >, ) { const [item, trace] = mapper(this); - return new TraceableImpl(item, this.trace.addTrace(trace)); + const traces = Array.isArray(trace) ? trace : [trace]; + return new TraceableImpl( + item, + traces.reduce((trace, _trace) => trace.addTrace(_trace), this.trace), + ); + } + + public get() { + return this.item; } } diff --git a/u/trace/logger.ts b/u/trace/logger.ts index 4f3c856..a5739c8 100644 --- a/u/trace/logger.ts +++ b/u/trace/logger.ts @@ -1,6 +1,7 @@ import { isDebug, type ITrace, + type ITraceWith, type SideEffect, type Supplier, } from "@emprespresso/pengueno"; @@ -53,47 +54,55 @@ export const logWithLevel = ( } }; +export type LogTraceSupplier = ITraceWith<Supplier<string>>; + +const defaultTrace = () => `[${new Date().toISOString()}]`; export const LoggerImpl = console; +export class LogTrace implements ITrace<LogTraceSupplier> { + constructor( + private readonly logger: ILogger = LoggerImpl, + private readonly traces: Array<LogTraceSupplier> = [defaultTrace], + private readonly allowedLevels: Supplier<Array<LogLevel>> = + defaultAllowedLevels, + private readonly defaultLevel: LogLevel = LogLevel.INFO, + ) { + } -export type LogTraceSupplier = string | Supplier<string>; + public addTrace(trace: LogTraceSupplier): ITrace<LogTraceSupplier> { + return new LogTrace( + this.logger, + this.traces.concat(trace), + this.allowedLevels, + this.defaultLevel, + ); + } -const foldTraces = (traces: Array<LogTraceSupplier>) => { - const { line, level } = traces.reduce( - (acc: { line: string; level: number }, t) => { - const val = typeof t === "function" ? t() : t; - if (isLogLevel(val)) { - return { - ...acc, - level: Math.max(logLevelOrder.indexOf(val), acc.level), - }; - } - const prefix = [ - acc.line, - val, - ].join(" "); - return { ...acc, prefix }; - }, - { line: "", level: -1 }, - ); - return { line, level: logLevelOrder[level] ?? LogLevel.UNKNOWN }; -}; + public trace(trace: LogTraceSupplier) { + const { line, level: _level } = this.foldTraces(this.traces.concat(trace)); + if (!this.allowedLevels().includes(_level)) return; -const defaultTrace = () => `[${new Date().toISOString()}]`; -export const LogTrace = ( - logger: ILogger, - traces: Array<LogTraceSupplier> = [defaultTrace], - allowedLevels: Supplier<Array<LogLevel>> = defaultAllowedLevels, - defaultLevel: LogLevel = LogLevel.INFO, -): ITrace<LogTraceSupplier> => { - return { - addTrace: (trace: LogTraceSupplier) => - LogTrace(logger, traces.concat(trace), allowedLevels, defaultLevel), - trace: (trace: LogTraceSupplier) => { - const { line, level: _level } = foldTraces(traces.concat(trace)); - if (!allowedLevels().includes(_level)) return; + const level = _level === LogLevel.UNKNOWN ? this.defaultLevel : _level; + logWithLevel(this.logger, level)(`[${level}]${line}`); + } - const level = _level === LogLevel.UNKNOWN ? defaultLevel : _level; - logWithLevel(logger, level)(`[${level}]${line}`); - }, - }; -}; + private foldTraces(traces: Array<LogTraceSupplier>) { + const { line, level } = traces.reduce( + (acc: { line: string; level: number }, t) => { + const val = typeof t === "function" ? t() : t; + if (isLogLevel(val)) { + return { + ...acc, + level: Math.max(logLevelOrder.indexOf(val), acc.level), + }; + } + const prefix = [ + acc.line, + val, + ].join(" "); + return { ...acc, prefix }; + }, + { line: "", level: -1 }, + ); + return { line, level: logLevelOrder[level] ?? LogLevel.UNKNOWN }; + } +} diff --git a/u/trace/metrics.ts b/u/trace/metrics.ts new file mode 100644 index 0000000..a26ee5d --- /dev/null +++ b/u/trace/metrics.ts @@ -0,0 +1,133 @@ +import { + isObject, + type ITrace, + type ITraceWith, + type Mapper, + type SideEffect, +} from "@emprespresso/pengueno"; + +export enum Unit { + COUNT, + MILLISECONDS, +} + +export interface IMetric { + readonly count: IEmittableMetric; + readonly time: IEmittableMetric; + readonly failure: IMetric; + readonly success: IMetric; + readonly _isIMetric: true; +} +export const isIMetric = (t: unknown): t is IMetric => + isObject(t) && "_isIMetric" in t; + +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, + _isMetricValue: true as true, + emissionTimestamp: Date.now(), + value, + }; + } +} + +export class Metric implements IMetric { + constructor( + public readonly count: IEmittableMetric, + public readonly time: IEmittableMetric, + public readonly failure: Metric, + public readonly success: Metric, + public readonly _isIMetric: true = true, + ) {} + + static fromName(name: string): Metric { + return new Metric( + new EmittableMetric(`${name}.count`, Unit.COUNT), + new EmittableMetric(`${name}.elapsed`, Unit.MILLISECONDS), + Metric.fromName(`${name}.failure`), + Metric.fromName(`${name}.success`), + ); + } +} + +export interface MetricValue { + readonly name: string; + readonly unit: Unit; + readonly value: number; + readonly emissionTimestamp: number; + readonly _isMetricValue: true; +} +export const isMetricValue = (t: unknown): t is MetricValue => + isObject(t) && "_isMetricValue" in t; + +export const isMetricsTraceSupplier = (t: unknown): t is MetricsTraceSupplier => + isMetricValue(t) || isIMetric(t); + +export type MetricsTraceSupplier = ITraceWith<IMetric | MetricValue>; +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 (isMetricValue(trace) || typeof trace === "string") return this; + return new MetricsTrace(this.metricConsumer)._nowTracing(trace); + } + + public trace(metric: MetricsTraceSupplier) { + if (typeof metric === "string") return this; + if (isMetricValue(metric)) { + this.metricConsumer([metric]); + return this; + } + + const foundMetricValues = this.tracing.flatMap(( + [tracing, startedTracing], + ) => + [tracing, tracing.success, tracing.failure] + .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 { + this.tracing.push([metric, new Date()]); + return this; + } +} diff --git a/u/trace/mod.ts b/u/trace/mod.ts index 9c42858..0f9b61b 100644 --- a/u/trace/mod.ts +++ b/u/trace/mod.ts @@ -1,3 +1,5 @@ export * from "./itrace.ts"; +export * from "./util.ts"; export * from "./logger.ts"; +export * from "./metrics.ts"; export * from "./trace.ts"; diff --git a/u/trace/trace.ts b/u/trace/trace.ts index 1d3d2d8..72d4eef 100644 --- a/u/trace/trace.ts +++ b/u/trace/trace.ts @@ -1,46 +1,82 @@ import { - type Callable, - type ITraceableMapper, - type ITraceableTuple, - LoggerImpl, + isMetricsTraceSupplier, + type ITrace, + type ITraceWith, LogTrace, type LogTraceSupplier, + MetricsTrace, + type MetricsTraceSupplier, + type MetricValue, TraceableImpl, } from "@emprespresso/pengueno"; export class LogTraceable<T> extends TraceableImpl<T, LogTraceSupplier> { + public static LogTrace = new LogTrace(); static from<T>(t: T) { - return new LogTraceable(t, LogTrace(LoggerImpl)); + return new LogTraceable(t, LogTraceable.LogTrace); } } -export class TraceUtil { - static withFunctionTrace<F extends Callable, T, Trace>( - f: F, - ): ITraceableMapper< - T, - ITraceableTuple<T, Trace>, - Trace - > { - return (t) => [t.item, `[${f.name}]`]; +const getEmbeddedMetricConsumer = + (logTrace: LogTrace) => (metrics: Array<MetricValue>) => + logTrace.addTrace("<metrics>").trace( + JSON.stringify(metrics, null, 2) + "</metrics>", + ); +export class EmbeddedMetricsTraceable<T> + extends TraceableImpl<T, MetricsTraceSupplier> { + public static MetricsTrace = new MetricsTrace( + getEmbeddedMetricConsumer(LogTraceable.LogTrace), + ); + + static from<T>(t: T) { + return new EmbeddedMetricsTraceable( + t, + EmbeddedMetricsTraceable.MetricsTrace, + ); } +} - static withClassTrace<C extends object, T, Trace>( - c: C, - ): ITraceableMapper< - T, - ITraceableTuple<T, Trace>, - Trace - > { - return (t) => [t.item, `[${c.constructor.name}]`]; +export type LogMetricTraceSupplier = ITraceWith< + LogTraceSupplier | MetricsTraceSupplier +>; +export class LogMetricTrace implements ITrace<LogMetricTraceSupplier> { + constructor( + private readonly logTrace: ITrace<LogTraceSupplier>, + private readonly metricsTrace: ITrace<MetricsTraceSupplier>, + ) {} + + public addTrace( + trace: LogTraceSupplier | MetricsTraceSupplier, + ): LogMetricTrace { + if (isMetricsTraceSupplier(trace)) { + this.metricsTrace.addTrace(trace); + return this; + } + this.logTrace.addTrace(trace); + return this; } - 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.item).map(mapper) - ).item; + public trace(trace: LogTraceSupplier | MetricsTraceSupplier) { + if (isMetricsTraceSupplier(trace)) { + this.metricsTrace.trace(trace); + return this; + } + this.logTrace.trace(trace); + return this; + } +} + +export class LogMetricTraceable<T> + extends TraceableImpl<T, MetricsTraceSupplier | LogTraceSupplier> { + public static LogMetricTrace = new LogMetricTrace( + LogTraceable.LogTrace, + EmbeddedMetricsTraceable.MetricsTrace, + ); + + static from<T>(t: T) { + return new LogMetricTraceable( + t, + LogMetricTraceable.LogMetricTrace, + ); } } diff --git a/u/trace/util.ts b/u/trace/util.ts new file mode 100644 index 0000000..dd8fb0d --- /dev/null +++ b/u/trace/util.ts @@ -0,0 +1,48 @@ +import type { + Callable, + IMetric, + ITraceableMapper, + ITraceableTuple, + MetricsTraceSupplier, +} from "@emprespresso/pengueno"; + +export class TraceUtil { + static withMetricTrace<T, Trace extends MetricsTraceSupplier>( + metric: IMetric, + ): ITraceableMapper< + T, + ITraceableTuple<T, Trace | Array<Trace>>, + Trace + > { + return (t) => [t.get(), metric as Trace]; + } + + static withFunctionTrace<F extends Callable, T, Trace>( + f: F, + ): ITraceableMapper< + T, + ITraceableTuple<T, Trace | Array<Trace>>, + Trace + > { + return (t) => [t.get(), `[${f.name}]`]; + } + + static withClassTrace<C extends object, T, Trace>( + c: C, + ): ITraceableMapper< + T, + ITraceableTuple<T, Trace | Array<Trace>>, + Trace + > { + return (t) => [t.get(), `[${c.constructor.name}]`]; + } + + 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(); + } +} |