|
| 1 | +import { Config, ConfigProvider, Effect, Layer, Metric, Option, References, Schema } from "effect"; |
| 2 | +import { FetchHttpClient } from "effect/http"; |
| 3 | +import { OtlpMetrics, OtlpSerialization, OtlpTracer } from "effect/observability"; |
| 4 | +import { uuidv7 } from "uuidv7"; |
| 5 | + |
| 6 | +// Configured by the standard `OTEL_*` variables alone. A configuration that fails |
| 7 | +// to build runs the command without export rather than failing it. |
| 8 | + |
| 9 | +// The OTel spec defaults `OTEL_{TRACES,METRICS}_EXPORTER` to `otlp`; Effect reads an |
| 10 | +// unset list as none, so an endpoint alone would export nothing. |
| 11 | +const exporterDefaults = ConfigProvider.layerAdd( |
| 12 | + ConfigProvider.fromUnknown({ OTEL_TRACES_EXPORTER: ["otlp"], OTEL_METRICS_EXPORTER: ["otlp"] }), |
| 13 | +); |
| 14 | + |
| 15 | +// Effect's fiber metrics hook every fiber start and end, so they follow metrics export. |
| 16 | +const metricsEnabled = Config.all({ |
| 17 | + disabled: Config.Boolean("OTEL_SDK_DISABLED").pipe(Config.withDefault(false)), |
| 18 | + endpoint: Config.String("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT").pipe( |
| 19 | + Config.orElse(() => Config.String("OTEL_EXPORTER_OTLP_ENDPOINT")), |
| 20 | + Config.option, |
| 21 | + ), |
| 22 | + exporters: Config.Array(Schema.String, "OTEL_METRICS_EXPORTER"), |
| 23 | +}).pipe( |
| 24 | + Config.map( |
| 25 | + ({ disabled, endpoint, exporters }) => |
| 26 | + !disabled && Option.isSome(endpoint) && exporters.some((exporter) => exporter.trim().toLowerCase() === "otlp"), |
| 27 | + ), |
| 28 | +); |
| 29 | + |
| 30 | +const fiberMetrics = Layer.unwrap( |
| 31 | + Effect.gen(function* () { |
| 32 | + return (yield* metricsEnabled) ? Metric.enableRuntimeMetricsLayer : Layer.empty; |
| 33 | + }), |
| 34 | +); |
| 35 | + |
| 36 | +// Only spans marked `level: "Info"` are exported; the rest, Effect's own `sql.*` |
| 37 | +// among them, default to `Debug`. Effect drops a span's children with it, so every |
| 38 | +// span on the path from a command to a model or tool call must be `Info`. |
| 39 | +const levels = { all: "All", trace: "Trace", debug: "Debug", info: "Info", warn: "Warn", error: "Error" } as const; |
| 40 | + |
| 41 | +const traceLevels = Layer.unwrap( |
| 42 | + Effect.gen(function* () { |
| 43 | + const name = yield* Config.String("CODEWORK_TRACE_LEVEL").pipe(Config.withDefault("info")); |
| 44 | + const minimum = levels[name.trim().toLowerCase() as keyof typeof levels] ?? "Info"; |
| 45 | + return Layer.mergeAll( |
| 46 | + Layer.succeed(References.CurrentTraceLevel, "Debug"), |
| 47 | + Layer.succeed(References.MinimumTraceLevel, minimum), |
| 48 | + ); |
| 49 | + }), |
| 50 | +); |
| 51 | + |
| 52 | +interface OtlpResource { |
| 53 | + readonly serviceName: string; |
| 54 | + readonly serviceVersion: string; |
| 55 | + readonly attributes: Record<string, string>; |
| 56 | +} |
| 57 | + |
| 58 | +// Effect has no gRPC exporter: `grpc` exports nothing rather than sending HTTP to a gRPC port. |
| 59 | +const exporters = (resource: OtlpResource) => |
| 60 | + Layer.unwrap( |
| 61 | + Effect.gen(function* () { |
| 62 | + const protocol = yield* Config.String("OTEL_EXPORTER_OTLP_PROTOCOL").pipe(Config.withDefault("http/protobuf")); |
| 63 | + const format = { |
| 64 | + "http/json": OtlpSerialization.layerJson, |
| 65 | + "http/protobuf": OtlpSerialization.layerProtobuf, |
| 66 | + }[protocol.trim().toLowerCase()]; |
| 67 | + if (format === undefined) return Layer.empty; |
| 68 | + return Layer.mergeAll( |
| 69 | + OtlpTracer.layerFromConfig({ resource }), |
| 70 | + OtlpMetrics.layerFromConfig({ resource }), |
| 71 | + ).pipe(Layer.provide(format)); |
| 72 | + }), |
| 73 | + ); |
| 74 | + |
| 75 | +const channel = (version: string) => |
| 76 | + version.includes("-canary.") ? "canary" : version.includes("-dev.") ? "dev" : "latest"; |
| 77 | + |
| 78 | +const instanceId = uuidv7(); |
| 79 | + |
| 80 | +export const layer = (options: { readonly version: string; readonly command: string }) => { |
| 81 | + const resource: OtlpResource = { |
| 82 | + serviceName: "codework", |
| 83 | + serviceVersion: options.version, |
| 84 | + attributes: { |
| 85 | + "service.instance.id": instanceId, |
| 86 | + "deployment.environment.name": channel(options.version), |
| 87 | + "codework.command": options.command, |
| 88 | + }, |
| 89 | + }; |
| 90 | + return Layer.mergeAll(exporters(resource), fiberMetrics, traceLevels).pipe( |
| 91 | + Layer.provide(FetchHttpClient.layer), |
| 92 | + Layer.provide(exporterDefaults), |
| 93 | + Layer.catchCause(() => Layer.empty), |
| 94 | + ); |
| 95 | +}; |
| 96 | + |
| 97 | +export * as Otel from "./otel.ts"; |
0 commit comments