summaryrefslogtreecommitdiff
path: root/u/trace
diff options
context:
space:
mode:
Diffstat (limited to 'u/trace')
-rw-r--r--u/trace/itrace.ts107
-rw-r--r--u/trace/logger.ts108
-rw-r--r--u/trace/metrics.ts143
-rw-r--r--u/trace/mod.ts5
-rw-r--r--u/trace/trace.ts82
-rw-r--r--u/trace/util.ts58
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();
+ }
+}