diff options
author | Elizabeth Alexander Hunt <me@liz.coffee> | 2025-05-18 12:24:09 -0700 |
---|---|---|
committer | Elizabeth Alexander Hunt <me@liz.coffee> | 2025-05-18 12:24:09 -0700 |
commit | 9cf3fc0259730b7dcf47b3ab4a04369e39fb4614 (patch) | |
tree | a96d39b4f28d38e327376cbef7ba60dbaa95e111 /u/server | |
parent | ef51b25e4388cbdf3a27e23d9f1fa381ae20a5ad (diff) | |
download | ci-9cf3fc0259730b7dcf47b3ab4a04369e39fb4614.tar.gz ci-9cf3fc0259730b7dcf47b3ab4a04369e39fb4614.zip |
finish up pengueno
Diffstat (limited to 'u/server')
-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 |
10 files changed, 184 insertions, 201 deletions
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, ); } } |