import { isObject, type ITrace, type ITraceWith, type Mapper, type SideEffect, type Supplier, } from "@emprespresso/pengueno"; export enum Unit { COUNT, MILLISECONDS, } export interface IMetric { readonly count: IEmittableMetric; readonly time: IEmittableMetric; readonly failure?: IMetric; readonly success?: IMetric; readonly warn?: IMetric; readonly children: Supplier>; 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; } 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?: Metric, public readonly success?: Metric, public readonly warn?: Metric, 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 { constructor( private readonly metricConsumer: SideEffect>, private readonly tracing: Array = [], private readonly flushed: Set = 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 { 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; } }