summaryrefslogtreecommitdiff
path: root/u/trace
diff options
context:
space:
mode:
Diffstat (limited to 'u/trace')
-rw-r--r--u/trace/index.ts6
-rw-r--r--u/trace/itrace.ts75
-rw-r--r--u/trace/log/ansi.ts15
-rw-r--r--u/trace/log/index.ts5
-rw-r--r--u/trace/log/level.ts19
-rw-r--r--u/trace/log/logger.ts5
-rw-r--r--u/trace/log/pretty_json_console.ts39
-rw-r--r--u/trace/log/trace.ts60
-rw-r--r--u/trace/logger.ts126
-rw-r--r--u/trace/metric/emittable.ts18
-rw-r--r--u/trace/metric/index.ts41
-rw-r--r--u/trace/metric/metric.ts54
-rw-r--r--u/trace/metric/trace.ts59
-rw-r--r--u/trace/metrics.ts140
-rw-r--r--u/trace/trace.ts14
-rw-r--r--u/trace/util.ts72
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}`);
}
}