summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Alexander Hunt <me@liz.coffee>2025-05-18 12:24:09 -0700
committerElizabeth Alexander Hunt <me@liz.coffee>2025-05-18 12:24:09 -0700
commit9cf3fc0259730b7dcf47b3ab4a04369e39fb4614 (patch)
treea96d39b4f28d38e327376cbef7ba60dbaa95e111
parentef51b25e4388cbdf3a27e23d9f1fa381ae20a5ad (diff)
downloadci-9cf3fc0259730b7dcf47b3ab4a04369e39fb4614.tar.gz
ci-9cf3fc0259730b7dcf47b3ab4a04369e39fb4614.zip
finish up pengueno
-rw-r--r--u/fn/callable.ts9
-rw-r--r--u/server/activity/fourohfour.ts23
-rw-r--r--u/server/activity/health.ts45
-rw-r--r--u/server/activity/mod.ts4
-rw-r--r--u/server/filter/json.ts38
-rw-r--r--u/server/filter/method.ts21
-rw-r--r--u/server/filter/mod.ts10
-rw-r--r--u/server/metrics.ts112
-rw-r--r--u/server/mod.ts6
-rw-r--r--u/server/request.ts64
-rw-r--r--u/server/response.ts62
-rw-r--r--u/trace/itrace.ts29
-rw-r--r--u/trace/logger.ts87
-rw-r--r--u/trace/metrics.ts133
-rw-r--r--u/trace/mod.ts2
-rw-r--r--u/trace/trace.ts94
-rw-r--r--u/trace/util.ts48
17 files changed, 506 insertions, 281 deletions
diff --git a/u/fn/callable.ts b/u/fn/callable.ts
index a087928..fc6ea81 100644
--- a/u/fn/callable.ts
+++ b/u/fn/callable.ts
@@ -7,9 +7,6 @@ export interface Supplier<T> extends Callable<T, undefined> {
(): T;
}
-export interface SideEffect<T> extends Callable<void, T> {
-}
-
export interface Mapper<T, U> extends Callable<U, T> {
(t: T): U;
}
@@ -17,3 +14,9 @@ export interface Mapper<T, U> extends Callable<U, T> {
export interface BiMapper<T, U, R> extends Callable {
(t: T, u: U): R;
}
+
+export interface SideEffect<T> extends Mapper<T, void> {
+}
+
+export interface BiSideEffect<T, U> extends BiMapper<T, U, void> {
+}
diff --git a/u/server/activity/fourohfour.ts b/u/server/activity/fourohfour.ts
index 698dacd..48740df 100644
--- a/u/server/activity/fourohfour.ts
+++ b/u/server/activity/fourohfour.ts
@@ -1,16 +1,10 @@
import {
type ITraceable,
JsonResponse,
- TraceUtil,
+ type PenguenoRequest,
+ type ServerTrace,
} from "@emprespresso/pengueno";
-export enum HealthCheckInput {
- CHECK,
-}
-export enum HealthCheckOutput {
- YAASQUEEN,
-}
-
const messages = [
"(≧ω≦)ゞ Oopsie! This endpoint has gone a-404-dable!",
"。゚(。ノωヽ。)゚。 Meow-t found! Your API call ran away!",
@@ -21,10 +15,13 @@ const messages = [
"(ꈍᴗꈍ) Uwu~ not found, but found our hearts instead!",
"ヽ(;▽;)ノ Eep! This route has ghosted you~",
];
-export const FourOhFourActivity = <Trace>(req: ITraceable<Request, Trace>) =>
- req.bimap(TraceUtil.withFunctionTrace(FourOhFourActivity))
- .map(() =>
- new JsonResponse(messages[Math.random() * messages.length], {
+const randomFourOhFour = () => messages[Math.random() * messages.length];
+export const FourOhFourActivity = (
+ req: ITraceable<PenguenoRequest, ServerTrace>,
+) =>
+ req
+ .move(
+ new JsonResponse(req, randomFourOhFour(), {
status: 404,
- })
+ }),
);
diff --git a/u/server/activity/health.ts b/u/server/activity/health.ts
index 98acbb8..b9efa3a 100644
--- a/u/server/activity/health.ts
+++ b/u/server/activity/health.ts
@@ -1,8 +1,12 @@
import {
type IEither,
type ITraceable,
+ JsonResponse,
LogLevel,
type Mapper,
+ Metric,
+ type PenguenoRequest,
+ type ServerTrace,
TraceUtil,
} from "@emprespresso/pengueno";
@@ -13,33 +17,36 @@ export enum HealthCheckOutput {
YAASQUEEN,
}
-export const HealthCheckActivity = <Trace>(
+const healthCheckMetric = Metric.fromName("Health");
+export const HealthCheckActivity = (
check: Mapper<
- ITraceable<HealthCheckInput, Trace>,
+ ITraceable<HealthCheckInput, ServerTrace>,
Promise<IEither<Error, HealthCheckOutput>>
>,
) =>
-(req: ITraceable<Request, Trace>) =>
- req.bimap(TraceUtil.withFunctionTrace(HealthCheckActivity))
- .flatMap((r) => r.move(HealthCheckInput.CHECK))
- .map(check)
- .map(TraceUtil.promiseify(({ item: health, trace }) => {
+(req: ITraceable<PenguenoRequest, ServerTrace>) =>
+ req
+ .bimap(TraceUtil.withFunctionTrace(HealthCheckActivity))
+ .bimap(TraceUtil.withMetricTrace(healthCheckMetric))
+ .flatMap((r) => r.move(HealthCheckInput.CHECK).map(check))
+ .map(TraceUtil.promiseify((h) => {
+ const health = h.get();
health.mapBoth((e) => {
- trace.addTrace(LogLevel.ERROR).trace(`${e}`);
- return new Response(
- JSON.stringify({
- message: "oh no, i need to eat more vegetables (。•́︿•̀。)...",
- }),
- { status: 500, headers: { "Content-Type": "application/json" } },
+ h.trace.trace(healthCheckMetric.failure);
+ h.trace.addTrace(LogLevel.ERROR).trace(`${e}`);
+ return new JsonResponse(
+ req,
+ "oh no, i need to eat more vegetables (。•́︿•̀。)...",
+ { status: 500 },
);
}, (_healthy) => {
+ h.trace.trace(healthCheckMetric.success);
const msg = `think im healthy!! (✿˘◡˘) ready to do work~`;
- trace.trace(msg);
- return new Response(
- JSON.stringify({
- message: "oh no, i need to eat more vegetables (。•́︿•̀。)...",
- }),
- { status: 500, headers: { "Content-Type": "application/json" } },
+ h.trace.trace(msg);
+ return new JsonResponse(
+ req,
+ msg,
+ { status: 200 },
);
});
}));
diff --git a/u/server/activity/mod.ts b/u/server/activity/mod.ts
index f0cbed2..9bd512f 100644
--- a/u/server/activity/mod.ts
+++ b/u/server/activity/mod.ts
@@ -1,4 +1,4 @@
-import { JsonResponse, type RequestFilter } from "@emprespresso/pengueno";
+import type { PenguenoResponse, RequestFilter } from "@emprespresso/pengueno";
export enum StatusOK {
FOLLOW = 300,
@@ -9,7 +9,7 @@ export interface ActivityOk {
}
export interface IActivity<Trace>
- extends RequestFilter<ActivityOk, Trace, Response> {
+ extends RequestFilter<ActivityOk, Trace, PenguenoResponse> {
}
export * from "./health.ts";
diff --git a/u/server/filter/json.ts b/u/server/filter/json.ts
index 1e05bad..c839707 100644
--- a/u/server/filter/json.ts
+++ b/u/server/filter/json.ts
@@ -3,34 +3,46 @@ import {
type IEither,
type ITraceable,
LogLevel,
+ type PenguenoRequest,
type RequestFilter,
+ type ServerTrace,
TraceUtil,
} from "@emprespresso/pengueno";
+import { Metric } from "../../trace/mod.ts";
-type JsonTransformer<R, Trace> = (
- json: ITraceable<unknown, Trace>,
+type JsonTransformer<R, ParsedJson = unknown> = (
+ json: ITraceable<ParsedJson, ServerTrace>,
) => IEither<Error, R>;
-export const json = <BodyT, Trace>(
- jsonTransformer: JsonTransformer<BodyT, Trace>,
-): RequestFilter<BodyT, Trace, Error> =>
-(r: ITraceable<Request, Trace>) =>
- r.bimap(TraceUtil.withFunctionTrace(json))
- .map(({ item: request, trace }) =>
- Either.fromFailableAsync<Error, BodyT>(request.json())
+
+const ParseJsonMetric = Metric.fromName("JsonParse");
+export const jsonModel = <MessageT>(
+ jsonTransformer: JsonTransformer<MessageT>,
+): RequestFilter<MessageT, Error> =>
+(r: ITraceable<PenguenoRequest, ServerTrace>) =>
+ r
+ .bimap(TraceUtil.withMetricTrace(ParseJsonMetric))
+ .map((j) =>
+ Either.fromFailableAsync<Error, MessageT>(j.get().json())
.then((either) =>
either.mapLeft((errReason) => {
- trace.addTrace(LogLevel.WARN).trace(`${errReason}`);
+ j.trace.addTrace(LogLevel.WARN).trace(`${errReason}`);
return new Error("seems to be invalid JSON (>//<) can you fix?");
})
)
)
.flatMapAsync(
TraceUtil.promiseify((traceableEitherJson) =>
- traceableEitherJson.map(({ item }) =>
- item.mapRight(traceableEitherJson.move).flatMap(
+ traceableEitherJson.map((t) =>
+ t.get().mapRight(traceableEitherJson.move).flatMap(
jsonTransformer,
)
)
),
)
- .item;
+ .peek(TraceUtil.promiseify((traceableEither) =>
+ traceableEither.get().mapBoth(
+ () => traceableEither.trace.trace(ParseJsonMetric.failure),
+ () => traceableEither.trace.trace(ParseJsonMetric.success),
+ )
+ ))
+ .get();
diff --git a/u/server/filter/method.ts b/u/server/filter/method.ts
index 8d13406..350f04c 100644
--- a/u/server/filter/method.ts
+++ b/u/server/filter/method.ts
@@ -3,7 +3,9 @@ import {
type ITraceable,
JsonResponse,
LogLevel,
+ type PenguenoRequest,
type RequestFilter,
+ type ServerTrace,
TraceUtil,
} from "@emprespresso/pengueno";
@@ -18,23 +20,22 @@ type HttpMethod =
| "TRACE"
| "PATCH";
-export const requireMethod = <Trace>(
+export const requireMethod = (
methods: Array<HttpMethod>,
-): RequestFilter<HttpMethod, Trace, JsonResponse> =>
-(req: ITraceable<Request, Trace>) =>
+): RequestFilter<HttpMethod, JsonResponse> =>
+(req: ITraceable<PenguenoRequest, ServerTrace>) =>
req.bimap(TraceUtil.withFunctionTrace(requireMethod))
- .move(Promise.resolve(req.item))
- .map(TraceUtil.promiseify(({ item: request, trace }) => {
- const { method: _method } = request;
+ .move(Promise.resolve(req.get()))
+ .map(TraceUtil.promiseify((t) => {
+ const { method: _method } = t.get();
const method = <HttpMethod> _method;
if (!methods.includes(method)) {
const msg = "that's not how you pet me (⋟﹏⋞)~";
- trace.addTrace(LogLevel.WARN).trace(msg);
+ t.trace.addTrace(LogLevel.WARN).trace(msg);
return Either.left<JsonResponse, HttpMethod>(
- new JsonResponse(msg, { status: 405 }),
+ new JsonResponse(req, msg, { status: 405 }),
);
}
-
return Either.right<JsonResponse, HttpMethod>(method);
}))
- .item;
+ .get();
diff --git a/u/server/filter/mod.ts b/u/server/filter/mod.ts
index 78fbd00..22ddad5 100644
--- a/u/server/filter/mod.ts
+++ b/u/server/filter/mod.ts
@@ -1,10 +1,14 @@
-import type { IEither, ITraceable } from "@emprespresso/pengueno";
+import type {
+ IEither,
+ ITraceable,
+ PenguenoRequest,
+ ServerTrace,
+} from "@emprespresso/pengueno";
export interface RequestFilter<
T,
- Trace,
Err,
- RIn = ITraceable<Request, Trace>,
+ RIn = ITraceable<PenguenoRequest, ServerTrace>,
> {
(req: RIn): Promise<IEither<Err, T>>;
}
diff --git a/u/server/metrics.ts b/u/server/metrics.ts
deleted file mode 100644
index 05df967..0000000
--- a/u/server/metrics.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-import {
- type BiMapper,
- Either,
- type IEither,
- type ITraceable,
- type Mapper,
- type Supplier,
-} from "@emprespresso/pengueno";
-
-export enum Unit {
- COUNT,
- MILLISECONDS,
-}
-
-export interface IMetric<MetricT extends string, TUnit extends Unit> {
- readonly metric: MetricT;
- readonly unit: TUnit;
- readonly value: number;
- readonly emissionTimestamp: Date;
-}
-
-export type BaseMetricT = string;
-export interface CountMetric<MetricT extends BaseMetricT>
- extends IMetric<MetricT, Unit.COUNT> {
- readonly unit: Unit.COUNT;
-}
-
-export interface TimeMetric<MetricT extends BaseMetricT>
- extends IMetric<MetricT, Unit.MILLISECONDS> {
- readonly unit: Unit.MILLISECONDS;
-}
-
-export interface IMetricsData<
- MetricT extends BaseMetricT,
- Tracing,
- TraceW,
-> {
- addCount: BiMapper<MetricT, number, CountMetric<MetricT>>;
-
- stopwatch: BiMapper<
- MetricT,
- ITraceable<Tracing, TraceW>,
- ITraceable<MetricT, TraceW>
- >;
- endStopwatch: Mapper<
- ITraceable<MetricT, TraceW>,
- IEither<Error, TimeMetric<MetricT>>
- >;
-
- flush: Supplier<Array<IMetric<MetricT, Unit>>>;
-}
-
-export class TraceableMetricsData<MetricT extends BaseMetricT, Tracing, Trace>
- implements IMetricsData<MetricT, Tracing, Trace> {
- private readonly timers: Map<ITraceable<MetricT, Trace>, Date> = new Map();
- private metricBuffer: Array<IMetric<MetricT, Unit>> = [];
-
- private constructor() {}
-
- private addMetric<TUnit extends Unit>(
- metric: MetricT,
- unit: TUnit,
- value: number,
- ): IMetric<MetricT, TUnit> {
- const _metric = {
- metric,
- unit,
- value,
- emissionTimestamp: new Date(),
- };
- this.metricBuffer.push(_metric);
- return _metric;
- }
-
- public flush() {
- const metrics = [...this.metricBuffer];
- this.metricBuffer = [];
- return metrics;
- }
-
- public addCount(
- metric: MetricT,
- count: number,
- ): CountMetric<MetricT> {
- return this.addMetric(metric, Unit.COUNT, count);
- }
-
- public stopwatch(metric: MetricT, traceable: ITraceable<Tracing, Trace>) {
- const timer = traceable.move(metric);
- this.timers.set(timer, new Date());
- return timer;
- }
-
- public endStopwatch(
- stopwatch: ITraceable<MetricT, Trace>,
- ): IEither<Error, TimeMetric<MetricT>> {
- const now = new Date();
- if (this.timers.has(stopwatch)) {
- const timer = this.timers.get(stopwatch)!;
- const diff = now.getTime() - timer.getTime();
- this.timers.delete(stopwatch);
- return Either.right<Error, TimeMetric<MetricT>>(
- this.addMetric(stopwatch.item, Unit.MILLISECONDS, diff) as TimeMetric<
- MetricT
- >,
- );
- }
- return Either.left<Error, TimeMetric<MetricT>>(
- new Error("cannot stop stopwatch before starting it"),
- );
- }
-}
diff --git a/u/server/mod.ts b/u/server/mod.ts
index 50f82dd..866b5f9 100644
--- a/u/server/mod.ts
+++ b/u/server/mod.ts
@@ -1,3 +1,7 @@
-export * from "./response.ts";
+import type { LogMetricTraceSupplier } from "@emprespresso/pengueno";
+export type ServerTrace = LogMetricTraceSupplier;
+
export * from "./activity/mod.ts";
export * from "./filter/mod.ts";
+export * from "./response.ts";
+export * from "./request.ts";
diff --git a/u/server/request.ts b/u/server/request.ts
index 6c4e602..7aa9917 100644
--- a/u/server/request.ts
+++ b/u/server/request.ts
@@ -1,27 +1,57 @@
-import { ITrace } from "@emprespresso/pengueno";
-import { ITraceWith } from "../trace/mod.ts";
+import { LogMetricTraceable } from "@emprespresso/pengueno";
-class RequestTraceWith {
+const greetings = [
+ "hewwo :D",
+ "hiya cutie (✿◠‿◠)",
+ "boop! ૮・ᴥ・ა",
+ "sending virtual hugs! (づ。◕‿‿◕。)づ",
+ "stay pawsitive ₍^..^₎⟆",
+ "⋆。‧˚❆🐧❆˚‧。⋆",
+];
+const penguenoGreeting = () =>
+ greetings[Math.floor(Math.random() * greetings.length)];
+
+export class PenguenoRequest extends Request {
private constructor(
+ _input: URL,
+ _requestInit: RequestInit,
public readonly id: string,
- public readonly received: Date,
+ public readonly at: Date,
) {
+ super(_input, _requestInit);
}
- public static from() {
- const id = crypto.randomUUID();
- const received = new Date();
- return new RequestTraceWith(id, received);
- }
-}
-export class RequestTrace implements ITrace<RequestTraceWith> {
- public readonly requestTrace: RequestTraceWith;
- constructor(reques);
+ public baseResponseHeaders(): Record<string, string> {
+ const ServerRequestTime = this.at.getTime();
+ const ServerResponseTime = Date.now();
+ const DeltaTime = ServerResponseTime - ServerRequestTime;
+ const RequestId = this.id;
- public addTrace(_t: ITraceWith<RequestTraceWith>) {
- return;
+ return Object.entries({
+ RequestId,
+ ServerRequestTime,
+ ServerResponseTime,
+ DeltaTime,
+ Hai: penguenoGreeting(),
+ }).reduce((acc, [key, val]) => ({ ...acc, [key]: (val.toString()) }), {});
}
- addTrace: Mapper<ITraceWith<TraceWith>, ITrace<TraceWith>>;
- trace: SideEffect<ITraceWith<TraceWith>>;
+ public static from(
+ request: Request,
+ ): LogMetricTraceable<PenguenoRequest> {
+ const id = crypto.randomUUID();
+ const url = new URL(request.url);
+ const { pathname } = url;
+ const traceSupplier = () => `[${id} <- ${request.method}'d @ ${pathname}]`;
+ return LogMetricTraceable
+ .from(
+ new PenguenoRequest(
+ url,
+ { ...request },
+ id,
+ new Date(),
+ ),
+ )
+ .bimap((_request) => [_request.get(), traceSupplier]);
+ }
}
diff --git a/u/server/response.ts b/u/server/response.ts
index 59ca43d..c21819a 100644
--- a/u/server/response.ts
+++ b/u/server/response.ts
@@ -2,43 +2,83 @@ import {
type IEither,
isEither,
type ITraceable,
+ Metric,
+ type PenguenoRequest,
+ type ServerTrace,
} from "@emprespresso/pengueno";
export type ResponseBody = object | string;
-export type TResponseInit = ResponseInit & { status: number };
-const withJsonResponseType = (opts: TResponseInit): TResponseInit => {
+export type TResponseInit = ResponseInit & {
+ status: number;
+ headers?: Record<string, string>;
+};
+
+const getResponse = (
+ req: PenguenoRequest,
+ opts: TResponseInit,
+): TResponseInit => {
return {
...opts,
headers: {
- "Content-Type": "application/json",
+ ...(req.baseResponseHeaders()),
...(opts?.headers),
+ "Content-Type": (opts?.headers?.["Content-Type"] ?? "text/plain") +
+ "; charset=utf-8",
},
};
};
-export class JsonResponse extends Response {
+const ResponseCodeMetrics = [1, 2, 3, 4, 5].map((x) =>
+ Metric.fromName(`response.${x}xx`)
+);
+export const getResponseMetric = (status: number) => {
+ const index = (Math.floor(status / 100)) + 1;
+ return ResponseCodeMetrics[index] ?? ResponseCodeMetrics[5 - 1];
+};
+
+export class PenguenoResponse extends Response {
+ constructor(
+ req: ITraceable<PenguenoRequest, ServerTrace>,
+ msg: BodyInit,
+ opts: TResponseInit,
+ ) {
+ const responseOpts = getResponse(req.get(), opts);
+ const resMetric = getResponseMetric(opts.status);
+ req.trace.trace(resMetric.count.withValue(1.0));
+ responseOpts.headers;
+ super(msg, responseOpts);
+ }
+}
+
+export class JsonResponse extends PenguenoResponse {
constructor(
- e: ITraceable<IEither<ResponseBody, ResponseBody>>,
+ req: ITraceable<PenguenoRequest, ServerTrace>,
+ e: BodyInit | IEither<ResponseBody, ResponseBody>,
opts: TResponseInit,
) {
- const responseOpts = withJsonResponseType(opts);
- const baseBody = {
- responseTime: Date.now(),
+ const optsWithJsonContentType = {
+ ...opts,
+ headers: {
+ ...opts?.headers,
+ "Content-Type": "application/json",
+ },
};
if (isEither<ResponseBody, ResponseBody>(e)) {
super(
+ req,
JSON.stringify(
e.fold((err, ok) => err ? ({ error: err! }) : ({ ok: ok! })),
),
- responseOpts,
+ optsWithJsonContentType,
);
return;
}
super(
+ req,
JSON.stringify(
- (Math.floor(responseOpts.status / 100) < 4) ? { ok: e } : { error: e },
+ (Math.floor(opts.status / 100) < 4) ? { ok: e } : { error: e },
),
- responseOpts,
+ optsWithJsonContentType,
);
}
}
diff --git a/u/trace/itrace.ts b/u/trace/itrace.ts
index b9b750d..620fff0 100644
--- a/u/trace/itrace.ts
+++ b/u/trace/itrace.ts
@@ -1,4 +1,4 @@
-import type { Mapper, SideEffect } from "@emprespresso/pengueno";
+import type { Mapper, SideEffect, Supplier } from "@emprespresso/pengueno";
// the "thing" every Trace writer must "trace()"
type BaseTraceWith = string;
@@ -19,15 +19,18 @@ export type ITraceableMapper<
) => U;
export interface ITraceable<T, Trace = BaseTraceWith> {
- readonly item: T;
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, Trace>, Trace>,
+ mapper: ITraceableMapper<
+ T,
+ ITraceableTuple<U, Array<Trace> | Trace>,
+ Trace
+ >,
) => ITraceable<U, Trace>;
peek: (peek: ITraceableMapper<T, void, Trace>) => ITraceable<T, Trace>;
flatMap: <U>(
@@ -40,8 +43,8 @@ export interface ITraceable<T, Trace = BaseTraceWith> {
export class TraceableImpl<T, TraceWith> implements ITraceable<T, TraceWith> {
protected constructor(
- readonly item: T,
- readonly trace: ITrace<TraceWith>,
+ private readonly item: T,
+ public readonly trace: ITrace<TraceWith>,
) {}
public map<U>(
@@ -69,7 +72,7 @@ export class TraceableImpl<T, TraceWith> implements ITraceable<T, TraceWith> {
>,
): ITraceable<Promise<U>, TraceWith> {
return new TraceableImpl(
- mapper(this).then(({ item }) => item),
+ mapper(this).then((t) => t.get()),
this.trace,
);
}
@@ -86,11 +89,19 @@ export class TraceableImpl<T, TraceWith> implements ITraceable<T, TraceWith> {
public bimap<U>(
mapper: ITraceableMapper<
T,
- ITraceableTuple<U, TraceWith>,
+ ITraceableTuple<U, Array<TraceWith> | TraceWith>,
TraceWith
>,
) {
const [item, trace] = mapper(this);
- return new TraceableImpl(item, this.trace.addTrace(trace));
+ 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
index 4f3c856..a5739c8 100644
--- a/u/trace/logger.ts
+++ b/u/trace/logger.ts
@@ -1,6 +1,7 @@
import {
isDebug,
type ITrace,
+ type ITraceWith,
type SideEffect,
type Supplier,
} from "@emprespresso/pengueno";
@@ -53,47 +54,55 @@ export const logWithLevel = (
}
};
+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,
+ ) {
+ }
-export type LogTraceSupplier = string | Supplier<string>;
+ public addTrace(trace: LogTraceSupplier): ITrace<LogTraceSupplier> {
+ return new LogTrace(
+ this.logger,
+ this.traces.concat(trace),
+ this.allowedLevels,
+ this.defaultLevel,
+ );
+ }
-const 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 };
-};
+ public trace(trace: LogTraceSupplier) {
+ const { line, level: _level } = this.foldTraces(this.traces.concat(trace));
+ if (!this.allowedLevels().includes(_level)) return;
-const defaultTrace = () => `[${new Date().toISOString()}]`;
-export const LogTrace = (
- logger: ILogger,
- traces: Array<LogTraceSupplier> = [defaultTrace],
- allowedLevels: Supplier<Array<LogLevel>> = defaultAllowedLevels,
- defaultLevel: LogLevel = LogLevel.INFO,
-): ITrace<LogTraceSupplier> => {
- return {
- addTrace: (trace: LogTraceSupplier) =>
- LogTrace(logger, traces.concat(trace), allowedLevels, defaultLevel),
- trace: (trace: LogTraceSupplier) => {
- const { line, level: _level } = foldTraces(traces.concat(trace));
- if (!allowedLevels().includes(_level)) return;
+ const level = _level === LogLevel.UNKNOWN ? this.defaultLevel : _level;
+ logWithLevel(this.logger, level)(`[${level}]${line}`);
+ }
- const level = _level === LogLevel.UNKNOWN ? defaultLevel : _level;
- logWithLevel(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..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;
+ }
+}
diff --git a/u/trace/mod.ts b/u/trace/mod.ts
index 9c42858..0f9b61b 100644
--- a/u/trace/mod.ts
+++ b/u/trace/mod.ts
@@ -1,3 +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
index 1d3d2d8..72d4eef 100644
--- a/u/trace/trace.ts
+++ b/u/trace/trace.ts
@@ -1,46 +1,82 @@
import {
- type Callable,
- type ITraceableMapper,
- type ITraceableTuple,
- LoggerImpl,
+ 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, LogTrace(LoggerImpl));
+ return new LogTraceable(t, LogTraceable.LogTrace);
}
}
-export class TraceUtil {
- static withFunctionTrace<F extends Callable, T, Trace>(
- f: F,
- ): ITraceableMapper<
- T,
- ITraceableTuple<T, Trace>,
- Trace
- > {
- return (t) => [t.item, `[${f.name}]`];
+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,
+ );
}
+}
- static withClassTrace<C extends object, T, Trace>(
- c: C,
- ): ITraceableMapper<
- T,
- ITraceableTuple<T, Trace>,
- Trace
- > {
- return (t) => [t.item, `[${c.constructor.name}]`];
+export type LogMetricTraceSupplier = ITraceWith<
+ LogTraceSupplier | MetricsTraceSupplier
+>;
+export class LogMetricTrace implements ITrace<LogMetricTraceSupplier> {
+ constructor(
+ private readonly logTrace: ITrace<LogTraceSupplier>,
+ private readonly metricsTrace: ITrace<MetricsTraceSupplier>,
+ ) {}
+
+ public addTrace(
+ trace: LogTraceSupplier | MetricsTraceSupplier,
+ ): LogMetricTrace {
+ if (isMetricsTraceSupplier(trace)) {
+ this.metricsTrace.addTrace(trace);
+ return this;
+ }
+ this.logTrace.addTrace(trace);
+ return this;
}
- 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.item).map(mapper)
- ).item;
+ 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..dd8fb0d
--- /dev/null
+++ b/u/trace/util.ts
@@ -0,0 +1,48 @@
+import type {
+ Callable,
+ IMetric,
+ ITraceableMapper,
+ ITraceableTuple,
+ MetricsTraceSupplier,
+} from "@emprespresso/pengueno";
+
+export class TraceUtil {
+ 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 (t) => [t.get(), `[${f.name}]`];
+ }
+
+ static withClassTrace<C extends object, T, Trace>(
+ c: C,
+ ): ITraceableMapper<
+ T,
+ ITraceableTuple<T, Trace | Array<Trace>>,
+ Trace
+ > {
+ return (t) => [t.get(), `[${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();
+ }
+}