1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
3import * as Redacted from "effect/Redacted";
4import type { Input } from "../Input.ts";
5import * as Output from "../Output.ts";
6import { layerOtlp, type OtlpSignalOptions } from "../Telemetry.ts";
7import type { ApiToken } from "./ApiToken.ts";
8import type { Dataset } from "./Dataset.ts";
9
10/**
11 * A resource passed to the layer: either the module-scope declaration (an
12 * Effect that resolves to the instance, same as what capability bindings
13 * accept) or an already-yielded instance.
14 */
15export type ResourceInput<T> = T | Effect.Effect<T, never, any>;
16
17/**
18 * Options for {@link Telemetry}: an ingest {@link ApiToken} plus one
19 * {@link Dataset} per OTel signal to export.
20 */
21export interface AxiomTelemetryProps {
22 /**
23 * The ingest credential. Must have `ingest: ["create"]` capability on
24 * every dataset passed below.
25 */
26 token: ResourceInput<ApiToken>;
27 /** Dataset (kind `otel:traces:v1`) to export traces into. */
28 traces?: ResourceInput<Dataset> | undefined;
29 /** Dataset (kind `otel:logs:v1`) to export logs into. */
30 logs?: ResourceInput<Dataset> | undefined;
31 /** Dataset (kind `otel:metrics:v1`) to export metrics into. */
32 metrics?: ResourceInput<Dataset> | undefined;
33 /**
34 * The exported `service.name`.
35 * @default the deployed Function/Worker's physical name
36 */
37 serviceName?: Input<string> | undefined;
38}
39
40/**
41 * Resource declarations are Effects — yield them to get the instance with
42 * attribute Output accessors, same as `Binding.Service`'s callable does for
43 * capability bindings.
44 */
45const instance = <T>(resource: ResourceInput<T>): Effect.Effect<T> =>
46 Effect.isEffect(resource)
47 ? (resource as Effect.Effect<T>)
48 : Effect.succeed(resource);
49
50const signal = (
51 token: ApiToken,
52 dataset: Dataset | undefined,
53 urlAttr: "otelTracesEndpoint" | "otelLogsEndpoint" | "otelMetricsEndpoint",
54): OtlpSignalOptions | undefined =>
55 dataset === undefined
56 ? undefined
57 : {
58 url: dataset[urlAttr],
59 headers: {
60 Authorization: Output.map(
61 token.token,
62 (bearer) =>
63 Redacted.make(
64 `Bearer ${Redacted.value(bearer)}`,
65 ) as Redacted.Redacted<string>,
66 ),
67 "X-Axiom-Dataset": dataset.name,
68 },
69 };
70
71/**
72 * Export a Function/Worker's telemetry to Axiom.
73 *
74 * A binding layer over {@link layerOtlp | Alchemy.Telemetry.layerOtlp}:
75 * building it binds each dataset's OTLP endpoint and the ingest token's
76 * `Authorization` header (as a secret) onto the host, and at runtime the
77 * built-in exporter ships each signal to its dataset, flushed per event.
78 *
79 * Compose it into the Function/Worker's single `Effect.provide`:
80 *
81 * ```ts
82 * import * as Axiom from "alchemy/Axiom";
83 * import { Bucket } from "./bucket.ts";
84 * import { Ingest, Logs, Traces } from "./observability.ts";
85 *
86 * export default Cloudflare.Worker(
87 * "Worker",
88 * { main: import.meta.url },
89 * Effect.gen(function* () {
90 * // ...
91 * }).pipe(
92 * Effect.provide(
93 * Layer.mergeAll(
94 * Cloudflare.R2.ReadWriteBucketBinding,
95 * Axiom.Telemetry({ token: Ingest, traces: Traces, logs: Logs }),
96 * ),
97 * ),
98 * ),
99 * );
100 * ```
101 */
102export const Telemetry = (props: AxiomTelemetryProps): Layer.Layer<never> =>
103 Layer.unwrap(
104 Effect.gen(function* () {
105 // Declarations are yielded to instances (registering them on the
106 // Stack if the enclosing host is the first to reference them), so
107 // attribute accessors below produce real Outputs.
108 const token = yield* instance(props.token);
109 const traces = props.traces && (yield* instance(props.traces));
110 const logs = props.logs && (yield* instance(props.logs));
111 const metrics = props.metrics && (yield* instance(props.metrics));
112 return layerOtlp({
113 serviceName: props.serviceName,
114 traces: signal(token, traces, "otelTracesEndpoint"),
115 logs: signal(token, logs, "otelLogsEndpoint"),
116 metrics: signal(token, metrics, "otelMetricsEndpoint"),
117 });
118 }),
119 ) as Layer.Layer<never>;