diff options
Diffstat (limited to 'lib/trace')
-rw-r--r-- | lib/trace/index.ts | 5 | ||||
-rw-r--r-- | lib/trace/itrace.ts | 91 | ||||
-rw-r--r-- | lib/trace/log/ansi.ts | 15 | ||||
-rw-r--r-- | lib/trace/log/index.ts | 5 | ||||
-rw-r--r-- | lib/trace/log/level.ts | 19 | ||||
-rw-r--r-- | lib/trace/log/logger.ts | 5 | ||||
-rw-r--r-- | lib/trace/log/pretty_json_console.ts | 39 | ||||
-rw-r--r-- | lib/trace/log/trace.ts | 60 | ||||
-rw-r--r-- | lib/trace/metric/emittable.ts | 18 | ||||
-rw-r--r-- | lib/trace/metric/index.ts | 41 | ||||
-rw-r--r-- | lib/trace/metric/metric.ts | 54 | ||||
-rw-r--r-- | lib/trace/metric/trace.ts | 59 | ||||
-rw-r--r-- | lib/trace/trace.ts | 77 | ||||
-rw-r--r-- | lib/trace/util.ts | 59 |
14 files changed, 547 insertions, 0 deletions
diff --git a/lib/trace/index.ts b/lib/trace/index.ts new file mode 100644 index 0000000..7a37cb6 --- /dev/null +++ b/lib/trace/index.ts @@ -0,0 +1,5 @@ +export * from './itrace'; +export * from './metric'; +export * from './log'; +export * from './trace'; +export * from './util'; diff --git a/lib/trace/itrace.ts b/lib/trace/itrace.ts new file mode 100644 index 0000000..e2019fa --- /dev/null +++ b/lib/trace/itrace.ts @@ -0,0 +1,91 @@ +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> { + /** + * 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> = { 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>; + 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, Array<_T>, Trace>) => Array<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>; +} + +export class TraceableImpl<T, Trace> implements ITraceable<T, Trace> { + protected constructor( + private readonly item: T, + public readonly trace: ITrace<Trace>, + ) {} + + public map<_T>(mapper: ITraceableMapper<T, _T, Trace>) { + const result = mapper(this); + return new TraceableImpl(result, this.trace); + } + + public coExtend<_T>(mapper: ITraceableMapper<T, Array<_T>, Trace>): Array<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, Trace>>, Trace>, + ): ITraceable<Promise<_T>, Trace> { + return new TraceableImpl( + mapper(this).then((t) => t.get()), + this.trace, + ); + } + + 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, Trace> { + return this.map(() => t); + } + + 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() { + return this.item; + } +} diff --git a/lib/trace/log/ansi.ts b/lib/trace/log/ansi.ts new file mode 100644 index 0000000..7ff16a3 --- /dev/null +++ b/lib/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/lib/trace/log/index.ts b/lib/trace/log/index.ts new file mode 100644 index 0000000..c416026 --- /dev/null +++ b/lib/trace/log/index.ts @@ -0,0 +1,5 @@ +export * from './ansi'; +export * from './level'; +export * from './logger'; +export * from './pretty_json_console'; +export * from './trace'; diff --git a/lib/trace/log/level.ts b/lib/trace/log/level.ts new file mode 100644 index 0000000..027dd71 --- /dev/null +++ b/lib/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/lib/trace/log/logger.ts b/lib/trace/log/logger.ts new file mode 100644 index 0000000..37a7e3f --- /dev/null +++ b/lib/trace/log/logger.ts @@ -0,0 +1,5 @@ +import { LogLevel } from './level'; + +export interface ILogger { + readonly log: (level: LogLevel, ...args: string[]) => void; +} diff --git a/lib/trace/log/pretty_json_console.ts b/lib/trace/log/pretty_json_console.ts new file mode 100644 index 0000000..02cc48c --- /dev/null +++ b/lib/trace/log/pretty_json_console.ts @@ -0,0 +1,39 @@ +import { ANSI, LogLevel, ILogger } from '.'; + +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/lib/trace/log/trace.ts b/lib/trace/log/trace.ts new file mode 100644 index 0000000..3f71e06 --- /dev/null +++ b/lib/trace/log/trace.ts @@ -0,0 +1,60 @@ +import { isDebug, ITrace, ITraceWith, memoize, Supplier } from '@emprespresso/pengueno'; +import { ILogger, isLogLevel, LogLevel, logLevelOrder, PrettyJsonConsoleLogger } from '.'; + +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/lib/trace/metric/emittable.ts b/lib/trace/metric/emittable.ts new file mode 100644 index 0000000..232cd3a --- /dev/null +++ b/lib/trace/metric/emittable.ts @@ -0,0 +1,18 @@ +import { IEmittableMetric, MetricValue, MetricValueTag, Unit } from '.'; + +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/lib/trace/metric/index.ts b/lib/trace/metric/index.ts new file mode 100644 index 0000000..aebc890 --- /dev/null +++ b/lib/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'; +export * from './metric'; +export * from './trace'; diff --git a/lib/trace/metric/metric.ts b/lib/trace/metric/metric.ts new file mode 100644 index 0000000..03ae4fe --- /dev/null +++ b/lib/trace/metric/metric.ts @@ -0,0 +1,54 @@ +import { EmittableMetric, IMetric, IMetricTag, IResultMetric, Unit } from '.'; + +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/lib/trace/metric/trace.ts b/lib/trace/metric/trace.ts new file mode 100644 index 0000000..0c5fe37 --- /dev/null +++ b/lib/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/lib/trace/trace.ts b/lib/trace/trace.ts new file mode 100644 index 0000000..ab7e841 --- /dev/null +++ b/lib/trace/trace.ts @@ -0,0 +1,77 @@ +import { + isMetricsTraceSupplier, + type ITrace, + type ITraceable, + type ITraceWith, + LogLevel, + LogTrace, + type LogTraceSupplier, + MetricsTrace, + type MetricsTraceSupplier, + type MetricValue, + TraceableImpl, +} from '.'; + +export class LogTraceable<T> extends TraceableImpl<T, LogTraceSupplier> { + public static LogTrace = new LogTrace(); + static of<T>(t: T) { + return new LogTraceable(t, LogTraceable.LogTrace); + } +} + +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)); + + static of<T>(t: T, metricsTrace = EmbeddedMetricsTraceable.MetricsTrace) { + return new EmbeddedMetricsTraceable(t, metricsTrace); + } +} + +export type LogMetricTraceSupplier = ITraceWith<LogTraceSupplier | MetricsTraceSupplier>; +export class LogMetricTrace implements ITrace<LogMetricTraceSupplier> { + constructor( + private logTrace: ITrace<LogTraceSupplier>, + private metricsTrace: ITrace<MetricsTraceSupplier>, + ) {} + + // public traceScope(trace: LogTraceSupplier | MetricsTraceSupplier): LogMetricTrace { + // if (isMetricsTraceSupplier(trace)) { + // this.metricsTrace = this.metricsTrace.traceScope(trace); + // return this; + // } + // this.logTrace = this.logTrace.traceScope(trace); + // return this; + // } + public traceScope(trace: LogTraceSupplier | MetricsTraceSupplier): LogMetricTrace { + if (isMetricsTraceSupplier(trace)) { + return new LogMetricTrace(this.logTrace, this.metricsTrace.traceScope(trace)); + } + return new LogMetricTrace(this.logTrace.traceScope(trace), this.metricsTrace); + } + + 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> { + static ofLogTraceable<T>(t: ITraceable<T, LogTraceSupplier>) { + const metricsTrace = new MetricsTrace(getEmbeddedMetricConsumer(t.trace)); + return new LogMetricTraceable(t.get(), new LogMetricTrace(t.trace, metricsTrace)); + } + + static of<T>(t: T) { + const logTrace = LogTraceable.of(t); + return LogMetricTraceable.ofLogTraceable(logTrace); + } +} diff --git a/lib/trace/util.ts b/lib/trace/util.ts new file mode 100644 index 0000000..ec67571 --- /dev/null +++ b/lib/trace/util.ts @@ -0,0 +1,59 @@ +import { + IEither, + IMetric, + isEither, + ITraceable, + ITraceWith, + LogLevel, + ResultMetric, + type Callable, + type ITraceableMapper, +} from '@emprespresso/pengueno'; + +export class TraceUtil { + 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 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 withTrace<T, Trace, _Trace extends ITraceWith<Trace>>( + trace: _Trace, + ): ITraceableMapper<T, ITraceable<T, Trace>, Trace> { + return (t) => t.traceScope(() => <Trace>trace); + } + + static withMetricTrace<T, Trace>(metric: IMetric): ITraceableMapper<T, ITraceable<T, Trace>, Trace> { + return TraceUtil.withTrace(<Trace>metric); + } + + 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}`); + } +} |