summaryrefslogtreecommitdiff
path: root/u/server
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 /u/server
parentef51b25e4388cbdf3a27e23d9f1fa381ae20a5ad (diff)
downloadci-9cf3fc0259730b7dcf47b3ab4a04369e39fb4614.tar.gz
ci-9cf3fc0259730b7dcf47b3ab4a04369e39fb4614.zip
finish up pengueno
Diffstat (limited to 'u/server')
-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
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,
);
}
}