1import type { ResourceAttributionEntry, ResourceAttributionSnapshot } from "@t3tools/contracts";
2import * as Context from "effect/Context";
3import * as DateTime from "effect/DateTime";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Ref from "effect/Ref";
7
8export interface ResourceAttributionRecord {
9 readonly component: string;
10 readonly operation: string;
11 readonly logicalReadBytes?: number;
12 readonly logicalWriteBytes?: number;
13 readonly count?: number;
14 readonly durationMs?: number;
15}
16
17export class ResourceAttribution extends Context.Service<
18 ResourceAttribution,
19 {
20 readonly record: (input: ResourceAttributionRecord) => Effect.Effect<void>;
21 readonly snapshot: Effect.Effect<ResourceAttributionSnapshot>;
22 }
23>()("t3/resourceTelemetry/ResourceAttribution") {}
24
25function key(input: Pick<ResourceAttributionRecord, "component" | "operation">): string {
26 return `${input.component}\u0000${input.operation}`;
27}
28
29function nonNegativeInteger(value: number | undefined, fallback: number): number {
30 if (value === undefined) return fallback;
31 if (!Number.isFinite(value)) return 0;
32 return Math.max(0, Math.round(value));
33}
34
35export const make = Effect.fn("resourceTelemetry.resourceAttribution.make")(function* () {
36 const entries = yield* Ref.make(new Map<string, ResourceAttributionEntry>());
37
38 const record: ResourceAttribution["Service"]["record"] = (input) =>
39 Ref.update(entries, (current) => {
40 const next = new Map(current);
41 const entryKey = key(input);
42 const existing = next.get(entryKey);
43 next.set(entryKey, {
44 component: input.component,
45 operation: input.operation,
46 logicalReadBytes:
47 (existing?.logicalReadBytes ?? 0) + nonNegativeInteger(input.logicalReadBytes, 0),
48 logicalWriteBytes:
49 (existing?.logicalWriteBytes ?? 0) + nonNegativeInteger(input.logicalWriteBytes, 0),
50 count: (existing?.count ?? 0) + nonNegativeInteger(input.count, 1),
51 durationMs: (existing?.durationMs ?? 0) + nonNegativeInteger(input.durationMs, 0),
52 });
53 return next;
54 });
55
56 return ResourceAttribution.of({
57 record,
58 snapshot: Effect.gen(function* () {
59 const readAt = yield* DateTime.now;
60 const current = yield* Ref.get(entries);
61 return {
62 readAt,
63 entries: [...current.values()].toSorted(
64 (left, right) => right.logicalWriteBytes - left.logicalWriteBytes,
65 ),
66 };
67 }),
68 });
69});
70
71export const layer = Layer.effect(ResourceAttribution, make());