1import * as HttpObservability from "@t3tools/shared/httpObservability";
2import { makeLocalFileTracer, makeTraceSink } from "@t3tools/shared/observability";
3import * as SharedObservability from "@t3tools/shared/observability";
4import * as OtelEnvironment from "@t3tools/shared/otelEnvironment";
5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
7import * as References from "effect/References";
8import * as Tracer from "effect/Tracer";
9import * as OtlpExporter from "effect/observability/OtlpExporter";
10import * as OtlpMetrics from "effect/observability/OtlpMetrics";
11import * as OtlpTracer from "effect/observability/OtlpTracer";
12import * as Statement from "effect/sql/Statement";
13
14import * as ServerConfig from "../config.ts";
15import * as ResourceAttribution from "../resourceTelemetry/ResourceAttribution.ts";
16import * as ServerLogger from "../serverLogger.ts";
17import * as BrowserTraceCollector from "./BrowserTraceCollector.ts";
18
19export const layer = Layer.unwrap(
20 Effect.gen(function* () {
21 const config = yield* ServerConfig.ServerConfig;
22
23 const traces = config.otlpTracesExport;
24 const metrics = config.otlpMetricsExport;
25 // The trace serializer stays in the returned context because the browser
26 // trace forwarder exports on the same signal.
27 const layerSerialization = SharedObservability.layerOtlpSerialization(traces.protocol);
28 const resource = ServerConfig.otlpResource(config);
29 const attribution = yield* ResourceAttribution.ResourceAttribution;
30
31 const layerTraceReferences = Layer.mergeAll(
32 Layer.succeed(Tracer.MinimumTraceLevel, config.traceMinLevel),
33 Layer.succeed(References.TracerTimingEnabled, config.traceTimingEnabled),
34 // Keeps each SQL statement's span current while it waits for a connection,
35 // so the client can record the lock wait on it.
36 Layer.succeed(Statement.SpanPropagationEnabled, true),
37 HttpObservability.layer,
38 );
39
40 const layerTracer = Layer.unwrap(
41 Effect.gen(function* () {
42 const sink = yield* makeTraceSink({
43 filePath: config.serverTracePath,
44 maxBytes: config.traceMaxBytes,
45 maxFiles: config.traceMaxFiles,
46 batchWindowMs: config.traceBatchWindowMs,
47 onFlush: (stats) =>
48 attribution.record({
49 component: "server-trace",
50 operation: "append",
51 logicalWriteBytes: stats.logicalWriteBytes,
52 count: stats.count,
53 durationMs: stats.durationMs,
54 }),
55 });
56 const delegate =
57 config.otlpTracesUrl === undefined
58 ? undefined
59 : yield* OtlpTracer.make({
60 url: config.otlpTracesUrl,
61 exportInterval: `${traces.exportIntervalMs} millis`,
62 headers: traces.headers,
63 resource,
64 });
65
66 const tracer = yield* makeLocalFileTracer({
67 filePath: config.serverTracePath,
68 maxBytes: config.traceMaxBytes,
69 maxFiles: config.traceMaxFiles,
70 batchWindowMs: config.traceBatchWindowMs,
71 sink,
72 ...(delegate ? { delegate } : {}),
73 });
74
75 return Layer.mergeAll(
76 Layer.succeed(Tracer.Tracer, tracer),
77 BrowserTraceCollector.layer(sink),
78 );
79 }),
80 ).pipe(Layer.provide(OtlpExporter.layerFlusher), Layer.provideMerge(layerSerialization));
81
82 const metricsLayer =
83 config.otlpMetricsUrl === undefined
84 ? Layer.empty
85 : OtlpMetrics.layer({
86 url: config.otlpMetricsUrl,
87 exportInterval: `${metrics.exportIntervalMs} millis`,
88 headers: metrics.headers,
89 resource,
90 }).pipe(Layer.provide(SharedObservability.layerOtlpSerialization(metrics.protocol)));
91
92 // Logged once the server's loggers are installed, so the warnings use them.
93 const layerOtelWarnings = Layer.effectDiscard(
94 Effect.forEach(config.otelEnvironment.warnings, (warning) => Effect.logWarning(warning)),
95 );
96
97 return layerOtelWarnings.pipe(
98 Layer.provideMerge(
99 Layer.mergeAll(ServerLogger.layer, layerTraceReferences, layerTracer, metricsLayer),
100 ),
101 Layer.provide(
102 OtelEnvironment.layerResourceAttributes(config.otelEnvironment.resourceAttributes),
103 ),
104 );
105 }),
106);