diff options
author | Elizabeth Alexander Hunt <me@liz.coffee> | 2025-05-12 09:40:12 -0700 |
---|---|---|
committer | Elizabeth <me@liz.coffee> | 2025-05-26 14:15:42 -0700 |
commit | d51c9d74857aca3c2f172609297266968bc7f809 (patch) | |
tree | 64327f9cc4219729aa11af32d7d4c70cddfc2292 /u/trace | |
parent | 30729a0cf707d9022bae0a7baaba77379dc31fd5 (diff) | |
download | ci-d51c9d74857aca3c2f172609297266968bc7f809.tar.gz ci-d51c9d74857aca3c2f172609297266968bc7f809.zip |
The big refactor TM
Diffstat (limited to 'u/trace')
-rw-r--r-- | u/trace/itrace.ts | 107 | ||||
-rw-r--r-- | u/trace/logger.ts | 108 | ||||
-rw-r--r-- | u/trace/metrics.ts | 143 | ||||
-rw-r--r-- | u/trace/mod.ts | 5 | ||||
-rw-r--r-- | u/trace/trace.ts | 82 | ||||
-rw-r--r-- | u/trace/util.ts | 58 |
6 files changed, 503 insertions, 0 deletions
diff --git a/u/trace/itrace.ts b/u/trace/itrace.ts new file mode 100644 index 0000000..e6189d3 --- /dev/null +++ b/u/trace/itrace.ts @@ -0,0 +1,107 @@ +import type { Mapper, SideEffect, Supplier } from "@emprespresso/pengueno"; + +// 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>>; + trace: SideEffect<ITraceWith<TraceWith>>; +} + +export type ITraceableTuple<T, TraceWith> = [T, BaseTraceWith | TraceWith]; +export type ITraceableMapper< + T, + U, + TraceWith, + W = ITraceable<T, TraceWith>, +> = ( + w: W, +) => U; + +export interface ITraceable<T, Trace = BaseTraceWith> { + 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, Array<Trace> | Trace>, + Trace + >, + ) => ITraceable<U, Trace>; + peek: (peek: ITraceableMapper<T, void, Trace>) => ITraceable<T, Trace>; + flatMap: <U>( + mapper: ITraceableMapper<T, ITraceable<U, Trace>, Trace>, + ) => ITraceable<U, Trace>; + flatMapAsync<U>( + mapper: ITraceableMapper<T, Promise<ITraceable<U, Trace>>, Trace>, + ): ITraceable<Promise<U>, Trace>; +} + +export class TraceableImpl<T, TraceWith> implements ITraceable<T, TraceWith> { + protected constructor( + private readonly item: T, + public readonly trace: ITrace<TraceWith>, + ) {} + + public map<U>( + mapper: ITraceableMapper<T, U, TraceWith>, + ) { + const result = mapper(this); + return new TraceableImpl(result, this.trace); + } + + public flatMap<U>( + mapper: ITraceableMapper< + T, + ITraceable<U, TraceWith>, + TraceWith + >, + ): ITraceable<U, TraceWith> { + return mapper(this); + } + + public flatMapAsync<U>( + mapper: ITraceableMapper< + T, + Promise<ITraceable<U, TraceWith>>, + TraceWith + >, + ): ITraceable<Promise<U>, TraceWith> { + return new TraceableImpl( + mapper(this).then((t) => t.get()), + this.trace, + ); + } + + public peek(peek: ITraceableMapper<T, void, TraceWith>) { + peek(this); + return this; + } + + public move<Tt>(t: Tt): ITraceable<Tt, TraceWith> { + return this.map(() => t); + } + + public bimap<U>( + mapper: ITraceableMapper< + T, + ITraceableTuple<U, 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 get() { + return this.item; + } +} diff --git a/u/trace/logger.ts b/u/trace/logger.ts new file mode 100644 index 0000000..a5739c8 --- /dev/null +++ b/u/trace/logger.ts @@ -0,0 +1,108 @@ +import { + isDebug, + type ITrace, + type ITraceWith, + type SideEffect, + type Supplier, +} from "@emprespresso/pengueno"; + +export interface ILogger { + log: (...args: unknown[]) => void; + debug: (...args: unknown[]) => void; + warn: (...args: unknown[]) => void; + error: (...args: unknown[]) => void; +} +export enum LogLevel { + UNKNOWN = "UNKNOWN", + INFO = "INFO", + WARN = "WARN", + DEBUG = "DEBUG", + ERROR = "ERROR", +} +const logLevelOrder: Array<LogLevel> = [ + LogLevel.DEBUG, + LogLevel.INFO, + LogLevel.WARN, + LogLevel.ERROR, +]; +export const isLogLevel = (l: string): l is LogLevel => + logLevelOrder.some((level) => <string> level === l); + +const defaultAllowedLevels = () => + [ + LogLevel.UNKNOWN, + ...(isDebug() ? [LogLevel.DEBUG] : []), + LogLevel.INFO, + LogLevel.WARN, + LogLevel.ERROR, + ] as Array<LogLevel>; + +export const logWithLevel = ( + logger: ILogger, + level: LogLevel, +): SideEffect<unknown> => { + switch (level) { + case LogLevel.UNKNOWN: + case LogLevel.INFO: + return logger.log; + case LogLevel.DEBUG: + return logger.debug; + case LogLevel.WARN: + return logger.warn; + case LogLevel.ERROR: + return logger.error; + } +}; + +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, + ) { + } + + public addTrace(trace: LogTraceSupplier): ITrace<LogTraceSupplier> { + return new LogTrace( + this.logger, + this.traces.concat(trace), + this.allowedLevels, + this.defaultLevel, + ); + } + + public trace(trace: LogTraceSupplier) { + const { line, level: _level } = this.foldTraces(this.traces.concat(trace)); + if (!this.allowedLevels().includes(_level)) return; + + const level = _level === LogLevel.UNKNOWN ? this.defaultLevel : _level; + logWithLevel(this.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..4ddde06 --- /dev/null +++ b/u/trace/metrics.ts @@ -0,0 +1,143 @@ +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<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: Metric, + public readonly success: Metric, + public readonly warn: Metric, + public readonly _tag: "IMetric" = "IMetric", + ) {} + + public children() { + return [this.failure, this.success, this.warn]; + } + + 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`), + Metric.fromName(`${name}.warn`), + ); + } +} + +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>; +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.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 { + this.tracing.push([metric, new Date()]); + return this; + } +} diff --git a/u/trace/mod.ts b/u/trace/mod.ts new file mode 100644 index 0000000..0f9b61b --- /dev/null +++ b/u/trace/mod.ts @@ -0,0 +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 new file mode 100644 index 0000000..e942066 --- /dev/null +++ b/u/trace/trace.ts @@ -0,0 +1,82 @@ +import { + 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, LogTraceable.LogTrace); + } +} + +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, + ); + } +} + +export type LogMetricTraceSupplier = ITraceWith< + LogTraceSupplier | MetricsTraceSupplier +>; +export class LogMetricTrace implements ITrace<LogMetricTraceSupplier> { + constructor( + private logTrace: ITrace<LogTraceSupplier>, + private metricsTrace: ITrace<MetricsTraceSupplier>, + ) {} + + public addTrace( + trace: LogTraceSupplier | MetricsTraceSupplier, + ): LogMetricTrace { + if (isMetricsTraceSupplier(trace)) { + this.metricsTrace = this.metricsTrace.addTrace(trace); + return this; + } + this.logTrace = this.logTrace.addTrace(trace); + return this; + } + + 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..302c8e4 --- /dev/null +++ b/u/trace/util.ts @@ -0,0 +1,58 @@ +import type { + Callable, + IMetric, + ITraceableMapper, + ITraceableTuple, + MetricsTraceSupplier, +} from "@emprespresso/pengueno"; + +export class TraceUtil { + static withTrace<T, Trace>( + trace: string, + ): ITraceableMapper< + T, + ITraceableTuple<T, Trace | Array<Trace>>, + Trace + > { + return (t) => [t.get(), `[${trace}]`]; + } + + 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 TraceUtil.withTrace(f.name); + } + + static withClassTrace<C extends object, T, Trace>( + c: C, + ): ITraceableMapper< + T, + ITraceableTuple<T, Trace | Array<Trace>>, + Trace + > { + return TraceUtil.withTrace(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(); + } +} |