summaryrefslogtreecommitdiff
path: root/u/trace/metric
diff options
context:
space:
mode:
Diffstat (limited to 'u/trace/metric')
-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
4 files changed, 172 insertions, 0 deletions
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);
+ }
+}