import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as Metric from "effect/Metric";
/**
* Counter for resource lifecycle operations. Tagged per call with
* `resource_type`, `op` (`precreate`/`create`/`update`/`delete`/`read`),
* and `status` (`success`/`error`).
*/
export const resourceCounter = Metric.counter("alchemy.resource.operations", {
description:
"Number of resource lifecycle operations dispatched by alchemy apply.",
incremental: true,
});
/**
* Histogram of how long each lifecycle operation takes.
*/
export const resourceDuration = Metric.timer("alchemy.resource.duration", {
description:
"Wall-clock duration of resource lifecycle operations dispatched by alchemy apply.",
});
/**
* Counter for CLI command invocations.
*/
export const cliCounter = Metric.counter("alchemy.cli.invocations", {
description: "Number of alchemy CLI command invocations.",
incremental: true,
});
/**
* Counter for Cloudflare State Store bootstrap/deploy operations.
* Tagged per call with `op` (e.g. `deploy`) and `status`
* (`success`/`error`) so that the deploy success rate of the
* state store itself can be tracked separately from regular
* resource lifecycle ops.
*/
export const stateStoreCounter = Metric.counter(
"alchemy.state_store.operations",
{
description:
"Number of Cloudflare State Store deploy/bootstrap operations dispatched by alchemy.",
incremental: true,
},
);
export type StateStoreOp = "deploy";
/**
* Counter for State store layer construction. Tagged with `id` (the
* `StateService.id` slug) so we can answer "how many distinct projects
* are using each backend" from the corresponding `state_store.init`
* spans. Open-ended on purpose: third-party state stores get counted
* automatically by setting their `StateService.id`.
*/
export const stateStoreInitCounter = Metric.counter(
"alchemy.state_store.inits",
{
description: "Number of times a State store layer is constructed.",
incremental: true,
},
);
/**
* Wraps a resource lifecycle Effect to record a counter + timer entry,
* tagged with `resource_type`, `op`, and `status` (`success`/`error`).
*
* Usage:
* ```ts
* provider.reconcile(input).pipe(recordResourceOp(node.resource.Type, "create"))
* ```
*/
export const recordResourceOp =
(resourceType: string, op: ResourceOp) =>
(self: Effect.Effect): Effect.Effect =>
Effect.suspend(() => {
const startNs = process.hrtime.bigint();
const baseAttrs = { resource_type: resourceType, op } as const;
return self.pipe(
Effect.onExit((exit) =>
recordOutcome(
baseAttrs,
Exit.isSuccess(exit) ? "success" : "error",
elapsed(startNs),
),
),
);
});
/**
* Wraps a CLI command Effect to bump {@link cliCounter} with the
* outcome (`success`/`error`).
*/
export const recordCli =
(command: string) =>
(self: Effect.Effect): Effect.Effect =>
self.pipe(
Effect.onExit((exit) =>
Metric.update(
Metric.withAttributes(cliCounter, {
command,
status: Exit.isSuccess(exit) ? "success" : "error",
}),
1,
),
),
);
/**
* Wraps a Cloudflare State Store deploy/bootstrap Effect to bump
* {@link stateStoreCounter} with the outcome (`success`/`error`),
* tagged with the given `op`.
*/
export const recordStateStoreOp =
(op: StateStoreOp) =>
(self: Effect.Effect): Effect.Effect =>
self.pipe(
Effect.onExit((exit) =>
Metric.update(
Metric.withAttributes(stateStoreCounter, {
op,
status: Exit.isSuccess(exit) ? "success" : "error",
}),
1,
),
),
);
/**
* Wraps a State store construction Effect to:
*
* 1. Bump {@link stateStoreInitCounter} tagged with `id`.
* 2. Open a `state_store.init` span carrying
* `alchemy.state_store.id` so Axiom (which can't query metric
* datasets via APL) can group projects by backend.
*
* The `id` is read off the constructed `StateService.id` field, so any
* third-party state-store implementation gets tracked just by setting
* a stable slug there. Apply at every `Layer.effect(State, …)` site
* exactly once.
*/
export const recordStateStoreInit = (
self: Effect.Effect,
): Effect.Effect =>
self.pipe(
Effect.tap((service) =>
Effect.all(
[
Metric.update(
Metric.withAttributes(stateStoreInitCounter, { id: service.id }),
1,
),
Effect.annotateCurrentSpan("alchemy.state_store.id", service.id),
],
{ discard: true },
),
),
Effect.withSpan("state_store.init"),
);
export type ResourceOp = "precreate" | "create" | "update" | "delete" | "read";
const elapsed = (startNs: bigint): Duration.Duration =>
Duration.nanos(process.hrtime.bigint() - startNs);
const recordOutcome = (
baseAttrs: { resource_type: string; op: ResourceOp },
status: "success" | "error",
duration: Duration.Duration,
): Effect.Effect => {
const attrs = { ...baseAttrs, status };
return Effect.all(
[
Metric.update(Metric.withAttributes(resourceCounter, attrs), 1),
Metric.update(Metric.withAttributes(resourceDuration, attrs), duration),
],
{ discard: true },
);
};