Open History to see the last commit
1import * as HostProcess from "@t3tools/shared/HostProcess";
2import * as SharedObservability from "@t3tools/shared/observability";
3import * as Cause from "effect/Cause";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Logger from "effect/Logger";
7import * as Redactable from "effect/Redactable";
8import * as References from "effect/References";
9import * as OtlpExporter from "effect/observability/OtlpExporter";
10import * as OtlpLogger from "effect/observability/OtlpLogger";
11
12import * as ServerConfig from "./config.ts";
13
14// HTTP errors logged with --log-ws-events carry the request URL and its connection ticket.
15const redactingCredentials = <Output>(logger: Logger.Logger<unknown, Output>) =>
16 Logger.make<unknown, Output>((options) =>
17 logger.log({ ...options, cause: SharedObservability.redactCauseCredentials(options.cause) }),
18 );
19
20// consolePretty collapses Cause fields via util.inspect; render them with Cause.pretty like standalone arguments.
21const consolePretty = (): Logger.Logger<unknown, void> => {
22 const pretty = redactingCredentials(Logger.consolePretty());
23 return Logger.make((options) =>
24 pretty.log({
25 ...options,
26 message: Array.isArray(options.message)
27 ? options.message.map(prettyCauseFields)
28 : prettyCauseFields(options.message),
29 }),
30 );
31};
32
33function prettyCauseFields(value: unknown): unknown {
34 if (typeof value !== "object" || value === null) return value;
35 const prototype: unknown = Object.getPrototypeOf(value);
36 if (prototype !== Object.prototype && prototype !== null) return value;
37 // A message with its own redaction hook already decides how it prints.
38 if (Redactable.isRedactable(value)) return value;
39 // Copy descriptors to preserve symbols without invoking getters.
40 const descriptors = Object.getOwnPropertyDescriptors(value);
41 let changed = false;
42 for (const descriptor of Object.values(descriptors)) {
43 if ("value" in descriptor && Cause.isCause(descriptor.value)) {
44 descriptor.value = Cause.pretty(descriptor.value);
45 changed = true;
46 }
47 }
48 return changed ? Object.create(prototype, descriptors) : value;
49}
50
51export const layer = Effect.gen(function* () {
52 const config = yield* ServerConfig.ServerConfig;
53 const hostName = yield* HostProcess.Hostname;
54 const layerMinimumLogLevel = Layer.succeed(References.MinimumLogLevel, config.logLevel);
55
56 const logs = config.otlpLogsExport;
57 const otlpLogger =
58 config.otlpLogsUrl === undefined
59 ? undefined
60 : OtlpLogger.make({
61 url: config.otlpLogsUrl,
62 exportInterval: `${logs.exportIntervalMs} millis`,
63 headers: logs.headers,
64 resource: ServerConfig.otlpResource(config, hostName),
65 }).pipe(Effect.map(redactingCredentials));
66
67 // Logger.layer replaces the whole set; name every logger here.
68 // OTLP includes traced messages as log records, so also using tracerLogger would duplicate in-span exports.
69 const layerLogger = Logger.layer(
70 otlpLogger === undefined
71 ? [consolePretty(), Logger.tracerLogger]
72 : [consolePretty(), otlpLogger],
73 { mergeWithExisting: false },
74 ).pipe(
75 Layer.provide(OtlpExporter.layerFlusher),
76 Layer.provide(SharedObservability.layerOtlpSerialization(logs.protocol)),
77 );
78
79 return Layer.mergeAll(layerLogger, layerMinimumLogLevel);
80}).pipe(Layer.unwrap);