summaryrefslogtreecommitdiff
path: root/u/trace/metrics.ts
diff options
context:
space:
mode:
Diffstat (limited to 'u/trace/metrics.ts')
-rw-r--r--u/trace/metrics.ts133
1 files changed, 133 insertions, 0 deletions
diff --git a/u/trace/metrics.ts b/u/trace/metrics.ts
new file mode 100644
index 0000000..a26ee5d
--- /dev/null
+++ b/u/trace/metrics.ts
@@ -0,0 +1,133 @@
+import {
+ isObject,
+ type ITrace,
+ type ITraceWith,
+ type Mapper,
+ type SideEffect,
+} from "@emprespresso/pengueno";
+
+export enum Unit {
+ COUNT,
+ MILLISECONDS,
+}
+
+export interface IMetric {
+ readonly count: IEmittableMetric;
+ readonly time: IEmittableMetric;
+ readonly failure: IMetric;
+ readonly success: IMetric;
+ readonly _isIMetric: true;
+}
+export const isIMetric = (t: unknown): t is IMetric =>
+ isObject(t) && "_isIMetric" in t;
+
+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,
+ _isMetricValue: true as true,
+ emissionTimestamp: Date.now(),
+ value,
+ };
+ }
+}
+
+export class Metric implements IMetric {
+ constructor(
+ public readonly count: IEmittableMetric,
+ public readonly time: IEmittableMetric,
+ public readonly failure: Metric,
+ public readonly success: Metric,
+ public readonly _isIMetric: true = true,
+ ) {}
+
+ 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`),
+ );
+ }
+}
+
+export interface MetricValue {
+ readonly name: string;
+ readonly unit: Unit;
+ readonly value: number;
+ readonly emissionTimestamp: number;
+ readonly _isMetricValue: true;
+}
+export const isMetricValue = (t: unknown): t is MetricValue =>
+ isObject(t) && "_isMetricValue" in t;
+
+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.success, tracing.failure]
+ .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;
+ }
+}