1import * as Clock from "effect/Clock";
2import * as Duration from "effect/Duration";
3import * as Effect from "effect/Effect";
4import * as Exit from "effect/Exit";
5import * as Metric from "effect/Metric";
6import { dual } from "effect/Function";
7
8import { compactMetricAttributes, outcomeFromExit } from "./Attributes.ts";
9
10export const rpcRequestsTotal = Metric.counter("t3_rpc_requests_total", {
11 description: "Total RPC requests handled by the websocket RPC server.",
12});
13
14export const rpcRequestDuration = Metric.timer("t3_rpc_request_duration", {
15 description: "RPC request handling duration.",
16});
17
18export const orchestrationEffectClaimsTotal = Metric.counter(
19 "t3_orchestration_effect_claims_total",
20 {
21 description: "Total completed orchestration effect outbox claim attempts by result.",
22 },
23);
24
25export const orchestrationEffectQueueWait = Metric.timer("t3_orchestration_effect_queue_wait", {
26 description:
27 "Time from an orchestration effect's temporal availability until claim, including same-thread blocking.",
28});
29
30export const providerSessionsTotal = Metric.counter("t3_provider_sessions_total", {
31 description: "Total provider session lifecycle operations.",
32});
33
34export const providerTurnsTotal = Metric.counter("t3_provider_turns_total", {
35 description: "Total provider turn lifecycle operations.",
36});
37
38export const providerTurnDuration = Metric.timer("t3_provider_turn_duration", {
39 description: "Time for the provider adapter to start a turn, not how long the turn runs.",
40});
41
42export const gitCommandsTotal = Metric.counter("t3_git_commands_total", {
43 description: "Total git commands executed by the server runtime.",
44});
45
46export const gitCommandDuration = Metric.timer("t3_git_command_duration", {
47 description: "Git command execution duration.",
48});
49
50export const terminalSessionsTotal = Metric.counter("t3_terminal_sessions_total", {
51 description: "Total terminal sessions started.",
52});
53
54export const terminalRestartsTotal = Metric.counter("t3_terminal_restarts_total", {
55 description: "Total terminal restart requests handled.",
56});
57
58/**
59 * One per webhook request that reached a task, by `outcome` (accepted,
60 * not_found, rejected_signature, disabled, rate_limited, queue_full, expired,
61 * prompt_too_long, error) and `source` (relay or direct).
62 */
63export const webhookDeliveriesTotal = Metric.counter("t3_webhook_deliveries_total", {
64 description: "Webhook requests handled, by outcome and source.",
65});
66
67export const webhookDeliveryDuration = Metric.timer("t3_webhook_delivery_duration", {
68 description: "Time to verify, log, and enqueue one webhook request.",
69});
70
71/** How long a relay-held request waited before this environment got it. */
72export const webhookHeldDelay = Metric.timer("t3_webhook_held_delay", {
73 description:
74 "Time between the relay receiving a webhook request and the environment handling it.",
75});
76
77/** Runs started by webhook deliveries, by `outcome` (started, skipped, failed). */
78export const webhookRunsTotal = Metric.counter("t3_webhook_runs_total", {
79 description: "Runs started from webhook deliveries, by outcome.",
80});
81
82/** Secrets agents asked users for, by how each ended: saved, declined, cancelled, timed_out. */
83export const secretRequestsTotal = Metric.counter("t3_secret_requests_total", {
84 description: "Secrets agents asked users for, by how each request ended.",
85});
86
87/** One-use secret refs a tool tried to use, by result: used, rejected. */
88export const secretRefsConsumedTotal = Metric.counter("t3_secret_refs_consumed_total", {
89 description: "Secret refs tools tried to use, by result.",
90});
91
92export const metricAttributes = (
93 attributes: Readonly<Record<string, unknown>>,
94): ReadonlyArray<[string, string]> => Object.entries(compactMetricAttributes(attributes));
95
96export const increment = (
97 metric: Metric.Metric<number, unknown>,
98 attributes: Readonly<Record<string, unknown>>,
99 amount = 1,
100) => Metric.update(Metric.withAttributes(metric, metricAttributes(attributes)), amount);
101
102export interface WithMetricsOptions {
103 readonly counter?: Metric.Metric<number, unknown>;
104 readonly timer?: Metric.Metric<Duration.Duration, unknown>;
105 readonly attributes?:
106 | Readonly<Record<string, unknown>>
107 | (() => Readonly<Record<string, unknown>>);
108 readonly outcomeAttributes?: (
109 outcome: ReturnType<typeof outcomeFromExit>,
110 ) => Readonly<Record<string, unknown>>;
111}
112
113const recordMetrics = (
114 options: WithMetricsOptions,
115 startedAt: bigint,
116 exit: Exit.Exit<unknown, unknown>,
117) =>
118 Effect.gen(function* () {
119 const duration = Duration.nanos((yield* Clock.monotonicTimeNanos) - startedAt);
120 const baseAttributes =
121 typeof options.attributes === "function" ? options.attributes() : (options.attributes ?? {});
122
123 if (options.timer) {
124 yield* Metric.update(
125 Metric.withAttributes(options.timer, metricAttributes(baseAttributes)),
126 duration,
127 );
128 }
129
130 if (options.counter) {
131 const outcome = outcomeFromExit(exit);
132 yield* Metric.update(
133 Metric.withAttributes(
134 options.counter,
135 metricAttributes({
136 ...baseAttributes,
137 outcome,
138 ...(options.outcomeAttributes ? options.outcomeAttributes(outcome) : {}),
139 }),
140 ),
141 1,
142 );
143 }
144 });
145
146// Durations come from the monotonic clock, so wall-clock corrections cannot skew them, and
147// metrics are recorded in an exit finalizer, so interrupted work is counted as "interrupt".
148const withMetricsImpl = <A, E, R>(
149 effect: Effect.Effect<A, E, R>,
150 options: WithMetricsOptions,
151): Effect.Effect<A, E, R> =>
152 Effect.flatMap(Clock.monotonicTimeNanos, (startedAt) =>
153 Effect.onExit(effect, (exit) => recordMetrics(options, startedAt, exit)),
154 );
155
156export const withMetrics: {
157 (
158 options: WithMetricsOptions,
159 ): <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
160 <A, E, R>(effect: Effect.Effect<A, E, R>, options: WithMetricsOptions): Effect.Effect<A, E, R>;
161} = dual(2, withMetricsImpl);