diff options
-rw-r--r-- | u/fn/callable.ts | 9 | ||||
-rw-r--r-- | u/server/activity/fourohfour.ts | 23 | ||||
-rw-r--r-- | u/server/activity/health.ts | 45 | ||||
-rw-r--r-- | u/server/activity/mod.ts | 4 | ||||
-rw-r--r-- | u/server/filter/json.ts | 38 | ||||
-rw-r--r-- | u/server/filter/method.ts | 21 | ||||
-rw-r--r-- | u/server/filter/mod.ts | 10 | ||||
-rw-r--r-- | u/server/metrics.ts | 112 | ||||
-rw-r--r-- | u/server/mod.ts | 6 | ||||
-rw-r--r-- | u/server/request.ts | 64 | ||||
-rw-r--r-- | u/server/response.ts | 62 | ||||
-rw-r--r-- | u/trace/itrace.ts | 29 | ||||
-rw-r--r-- | u/trace/logger.ts | 87 | ||||
-rw-r--r-- | u/trace/metrics.ts | 133 | ||||
-rw-r--r-- | u/trace/mod.ts | 2 | ||||
-rw-r--r-- | u/trace/trace.ts | 94 | ||||
-rw-r--r-- | u/trace/util.ts | 48 |
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(); + } +} |