| author | |
| committer | |
| log | 74ee5153e05eb183dabec413ff9025d83d2058e4 |
| tree | 9e2181eca5bf8f8d7b7e3b5ff73344f4d84b8ea8 |
| parent | 679c34c096193446ceed62e633387d91fe2cfff8 |
| signature | Signed by PGP key B5690EEEBB952194 |
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>4 files changed, 155 insertions(+), 9 deletions(-)
apps/desktop/src/backend/DesktopBackendConfiguration.ts+3| ... | @@ -118,6 +118,9 @@ const WSL_FORWARDED_ENV_NAMES = [ | ... | @@ -118,6 +118,9 @@ const WSL_FORWARDED_ENV_NAMES = [ |
| 118 | "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", | 118 | "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", |
| 119 | "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL", | 119 | "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL", |
| 120 | "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL", | 120 | "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL", |
| 121 | "OTEL_TRACES_EXPORTER", | ||
| 122 | "OTEL_METRICS_EXPORTER", | ||
| 123 | "OTEL_LOGS_EXPORTER", | ||
| 121 | ] as const; | 124 | ] as const; |
| 122 | 125 | ||
| 123 | const WSL_SERVER_SYSTEM_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; | 126 | const WSL_SERVER_SYSTEM_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; |
docs/operations/observability.md+5| ... | @@ -631,6 +631,11 @@ machine that sets `OTEL_SDK_DISABLED` for everything else. It accepts the usual | ... | @@ -631,6 +631,11 @@ machine that sets `OTEL_SDK_DISABLED` for everything else. It accepts the usual |
| 631 | OpenTelemetry specification and only `true` disables export, so `OTEL_SDK_DISABLED=1` does not. | 631 | OpenTelemetry specification and only `true` disables export, so `OTEL_SDK_DISABLED=1` does not. |
| 632 | Values are case-insensitive and trimmed. An unrecognized value is ignored with a startup warning. | 632 | Values are case-insensitive and trimmed. An unrecognized value is ignored with a startup warning. |
| 633 | 633 | ||
| 634 | `OTEL_TRACES_EXPORTER`, `OTEL_METRICS_EXPORTER`, or `OTEL_LOGS_EXPORTER` set to `none` turns off | ||
| 635 | just that signal, overriding an OTEL endpoint and the Settings endpoint. A `T3CODE_OTLP_*_URL` still | ||
| 636 | wins for its signal. `otlp` is the default, and any other exporter name, such as `console` or | ||
| 637 | `prometheus`, is ignored with a startup warning. | ||
| 638 | |||
| 634 | ### What Is Instrumented Today | 639 | ### What Is Instrumented Today |
| 635 | 640 | ||
| 636 | Current high-value span and metric boundaries include: | 641 | Current high-value span and metric boundaries include: |
packages/shared/src/otelEnvironment.test.ts+86| ... | @@ -204,6 +204,73 @@ describe("OtelEnvironment", () => { | ... | @@ -204,6 +204,73 @@ describe("OtelEnvironment", () => { |
| 204 | logs: "Unset", | 204 | logs: "Unset", |
| 205 | warnings: [T3_OFF], | 205 | warnings: [T3_OFF], |
| 206 | }, | 206 | }, |
| 207 | { | ||
| 208 | name: "an exporter of none turns off only its signal", | ||
| 209 | env: { | ||
| 210 | OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector:4318", | ||
| 211 | OTEL_LOGS_EXPORTER: "none", | ||
| 212 | }, | ||
| 213 | traces: "https://collector:4318/v1/traces", | ||
| 214 | metrics: "https://collector:4318/v1/metrics", | ||
| 215 | logs: "Off", | ||
| 216 | warnings: [], | ||
| 217 | }, | ||
| 218 | { | ||
| 219 | name: "an exporter of none turns its signal off with no endpoint named", | ||
| 220 | env: { OTEL_METRICS_EXPORTER: " NONE " }, | ||
| 221 | traces: "Unset", | ||
| 222 | metrics: "Off", | ||
| 223 | logs: "Unset", | ||
| 224 | warnings: [], | ||
| 225 | }, | ||
| 226 | { | ||
| 227 | name: "an exporter of none leaves its signal's endpoint unread", | ||
| 228 | env: { OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "not-a-url", OTEL_TRACES_EXPORTER: "none" }, | ||
| 229 | traces: "Off", | ||
| 230 | metrics: "Unset", | ||
| 231 | logs: "Unset", | ||
| 232 | warnings: [], | ||
| 233 | }, | ||
| 234 | { | ||
| 235 | name: "an exporter of otlp or blank is the default", | ||
| 236 | env: { | ||
| 237 | OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector:4318", | ||
| 238 | OTEL_TRACES_EXPORTER: "OTLP", | ||
| 239 | OTEL_LOGS_EXPORTER: " ", | ||
| 240 | }, | ||
| 241 | traces: "https://collector:4318/v1/traces", | ||
| 242 | metrics: "https://collector:4318/v1/metrics", | ||
| 243 | logs: "https://collector:4318/v1/logs", | ||
| 244 | warnings: [], | ||
| 245 | }, | ||
| 246 | { | ||
| 247 | name: "an exporter T3 Code does not have is ignored with a warning", | ||
| 248 | env: { | ||
| 249 | OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector:4318", | ||
| 250 | OTEL_METRICS_EXPORTER: "prometheus", | ||
| 251 | }, | ||
| 252 | traces: "https://collector:4318/v1/traces", | ||
| 253 | metrics: "https://collector:4318/v1/metrics", | ||
| 254 | logs: "https://collector:4318/v1/logs", | ||
| 255 | warnings: [ | ||
| 256 | "OTEL_METRICS_EXPORTER names prometheus, which T3 Code does not export to, so it was ignored", | ||
| 257 | ], | ||
| 258 | }, | ||
| 259 | { | ||
| 260 | name: "a list honors none and otlp and names what it ignored", | ||
| 261 | env: { | ||
| 262 | OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector:4318", | ||
| 263 | OTEL_TRACES_EXPORTER: "console,none", | ||
| 264 | OTEL_LOGS_EXPORTER: "otlp,console,otlpp", | ||
| 265 | }, | ||
| 266 | traces: "Off", | ||
| 267 | metrics: "https://collector:4318/v1/metrics", | ||
| 268 | logs: "https://collector:4318/v1/logs", | ||
| 269 | warnings: [ | ||
| 270 | "OTEL_TRACES_EXPORTER names console, which T3 Code does not export to, so it was ignored", | ||
| 271 | "OTEL_LOGS_EXPORTER names console, otlpp, which T3 Code does not export to, so they were ignored", | ||
| 272 | ], | ||
| 273 | }, | ||
| 207 | ])("$name", ({ env, traces, metrics, logs, warnings }) => | 274 | ])("$name", ({ env, traces, metrics, logs, warnings }) => |
| 208 | Effect.gen(function* () { | 275 | Effect.gen(function* () { |
| 209 | const resolved = yield* load(env); | 276 | const resolved = yield* load(env); |
| ... | @@ -396,6 +463,25 @@ describe("OtelEnvironment", () => { | ... | @@ -396,6 +463,25 @@ describe("OtelEnvironment", () => { |
| 396 | }); | 463 | }); |
| 397 | }); | 464 | }); |
| 398 | 465 | ||
| 466 | it.effect("an exporter of none keeps the Settings endpoint from re-enabling its signal", () => | ||
| 467 | Effect.gen(function* () { | ||
| 468 | const otel = yield* load({ OTEL_LOGS_EXPORTER: "none" }); | ||
| 469 | const t3 = { | ||
| 470 | url: undefined, | ||
| 471 | export: { protocol: "http/json", headers: undefined, exportIntervalMs: 10_000 }, | ||
| 472 | } as const; | ||
| 473 | assert.strictEqual( | ||
| 474 | OtelEnvironment.resolveSignalEndpoint(otel, "logs", t3, "http://settings:4318/v1/logs"), | ||
| 475 | undefined, | ||
| 476 | ); | ||
| 477 | assert.strictEqual( | ||
| 478 | OtelEnvironment.resolveSignalEndpoint(otel, "traces", t3, "http://settings:4318/v1/traces") | ||
| 479 | ?.url, | ||
| 480 | "http://settings:4318/v1/traces", | ||
| 481 | ); | ||
| 482 | }), | ||
| 483 | ); | ||
| 484 | |||
| 399 | describe("layerResourceAttributes", () => { | 485 | describe("layerResourceAttributes", () => { |
| 400 | it.effect.each([ | 486 | it.effect.each([ |
| 401 | { name: "a list that does not decode", raw: "team=%zz", attributes: [] }, | 487 | { name: "a list that does not decode", raw: "team=%zz", attributes: [] }, |
packages/shared/src/otelEnvironment.ts+61-9| ... | @@ -1,5 +1,5 @@ | ... | @@ -1,5 +1,5 @@ |
| 1 | /** | 1 | /** |
| 2 | * otelEnvironment: the OpenTelemetry kill switch and endpoint variables, | 2 | * otelEnvironment: the OpenTelemetry kill switch, exporter, and endpoint variables, |
| 3 | * shared by the server and the desktop main process so both agree on what | 3 | * shared by the server and the desktop main process so both agree on what |
| 4 | * turns export off and where it goes. | 4 | * turns export off and where it goes. |
| 5 | * | 5 | * |
| ... | @@ -23,8 +23,9 @@ type OtlpSignalName = "TRACES" | "METRICS" | "LOGS"; | ... | @@ -23,8 +23,9 @@ type OtlpSignalName = "TRACES" | "METRICS" | "LOGS"; |
| 23 | 23 | ||
| 24 | /** | 24 | /** |
| 25 | * What the OTEL variables say about one signal. `Off` is a signal they | 25 | * What the OTEL variables say about one signal. `Off` is a signal they |
| 26 | * claimed with an endpoint, protocol, or headers that do not read, so it is | 26 | * claimed with an endpoint, protocol, or headers that do not read, or turned |
| 27 | * exported nowhere rather than to the bootstrap or Settings collector. | 27 | * off with `OTEL_<SIGNAL>_EXPORTER=none`, so it is exported nowhere rather |
| 28 | * than to the bootstrap or Settings collector. | ||
| 28 | */ | 29 | */ |
| 29 | export type OtelSignal = Data.TaggedEnum<{ | 30 | export type OtelSignal = Data.TaggedEnum<{ |
| 30 | Unset: {}; | 31 | Unset: {}; |
| ... | @@ -179,6 +180,38 @@ const headers = (name: string) => | ... | @@ -179,6 +180,38 @@ const headers = (name: string) => |
| 179 | `${name} is not a list of key=value pairs with percent-encoded values, ${NOT_EXPORTED}`, | 180 | `${name} is not a list of key=value pairs with percent-encoded values, ${NOT_EXPORTED}`, |
| 180 | ); | 181 | ); |
| 181 | 182 | ||
| 183 | type Exporter = "otlp" | "none"; | ||
| 184 | |||
| 185 | const EXPORTERS: ReadonlySet<string> = new Set<Exporter>(["otlp", "none"]); | ||
| 186 | |||
| 187 | const isExporter = (entry: string): entry is Exporter => EXPORTERS.has(entry); | ||
| 188 | |||
| 189 | /** | ||
| 190 | * `OTEL_<SIGNAL>_EXPORTER`, a case-insensitive list whose default is `otlp`. | ||
| 191 | * Entries T3 Code has no exporter for are named in a warning and dropped, and | ||
| 192 | * a list left with nothing to honor reads as unset, as the specification asks | ||
| 193 | * of any enum value an implementation does not recognize. | ||
| 194 | */ | ||
| 195 | const exporter = (name: string): Config.Config<Setting<Exporter>> => | ||
| 196 | Config.String(name).pipe( | ||
| 197 | Config.option, | ||
| 198 | Config.map((option): Setting<Exporter> => { | ||
| 199 | const entries = (Option.getOrUndefined(option) ?? "") | ||
| 200 | .split(",") | ||
| 201 | .map((entry) => entry.trim().toLowerCase()) | ||
| 202 | .filter((entry) => entry !== ""); | ||
| 203 | const ignored = [...new Set(entries.filter((entry) => !isExporter(entry)))]; | ||
| 204 | const known = new Set(entries.filter(isExporter)); | ||
| 205 | const value = known.has("otlp") ? "otlp" : known.has("none") ? "none" : undefined; | ||
| 206 | return ignored.length === 0 | ||
| 207 | ? { value } | ||
| 208 | : { | ||
| 209 | value, | ||
| 210 | warning: `${name} names ${ignored.join(", ")}, which T3 Code does not export to, so ${ignored.length === 1 ? "it was" : "they were"} ignored`, | ||
| 211 | }; | ||
| 212 | }), | ||
| 213 | ); | ||
| 214 | |||
| 182 | interface Settings { | 215 | interface Settings { |
| 183 | readonly endpoint: Setting<URL>; | 216 | readonly endpoint: Setting<URL>; |
| 184 | readonly protocol: Setting<OtlpProtocol>; | 217 | readonly protocol: Setting<OtlpProtocol>; |
| ... | @@ -215,9 +248,23 @@ interface ResolvedSignal { | ... | @@ -215,9 +248,23 @@ interface ResolvedSignal { |
| 215 | /** | 248 | /** |
| 216 | * A signal whose endpoint, protocol, or headers do not read is not exported | 249 | * A signal whose endpoint, protocol, or headers do not read is not exported |
| 217 | * rather than sent somewhere, in a format, or without the credentials its | 250 | * rather than sent somewhere, in a format, or without the credentials its |
| 218 | * collector expects. | 251 | * collector expects. `none` turns the signal off before any of those are read, |
| 252 | * whether or not an endpoint was named. | ||
| 219 | */ | 253 | */ |
| 220 | const signal = (name: OtlpSignalName, own: Settings, generic: Settings): ResolvedSignal => { | 254 | const signal = ( |
| 255 | name: OtlpSignalName, | ||
| 256 | exporter: Setting<Exporter>, | ||
| 257 | own: Settings, | ||
| 258 | generic: Settings, | ||
| 259 | ): ResolvedSignal => { | ||
| 260 | if (exporter.value === "none") { | ||
| 261 | return { signal: OtelSignal.Off(), used: [exporter] }; | ||
| 262 | } | ||
| 263 | const resolved = endpointSignal(name, own, generic); | ||
| 264 | return { signal: resolved.signal, used: [exporter, ...resolved.used] }; | ||
| 265 | }; | ||
| 266 | |||
| 267 | const endpointSignal = (name: OtlpSignalName, own: Settings, generic: Settings): ResolvedSignal => { | ||
| 221 | const ownEndpoint = isClaimed(own.endpoint); | 268 | const ownEndpoint = isClaimed(own.endpoint); |
| 222 | const endpoint = ownEndpoint ? own.endpoint : generic.endpoint; | 269 | const endpoint = ownEndpoint ? own.endpoint : generic.endpoint; |
| 223 | if (endpoint.value === undefined) { | 270 | if (endpoint.value === undefined) { |
| ... | @@ -263,16 +310,21 @@ export const load: Effect.Effect<OtelEnvironment> = Config.all({ | ... | @@ -263,16 +310,21 @@ export const load: Effect.Effect<OtelEnvironment> = Config.all({ |
| 263 | traces: settings("OTEL_EXPORTER_OTLP_TRACES_"), | 310 | traces: settings("OTEL_EXPORTER_OTLP_TRACES_"), |
| 264 | metrics: settings("OTEL_EXPORTER_OTLP_METRICS_"), | 311 | metrics: settings("OTEL_EXPORTER_OTLP_METRICS_"), |
| 265 | logs: settings("OTEL_EXPORTER_OTLP_LOGS_"), | 312 | logs: settings("OTEL_EXPORTER_OTLP_LOGS_"), |
| 313 | exporters: Config.all({ | ||
| 314 | traces: exporter("OTEL_TRACES_EXPORTER"), | ||
| 315 | metrics: exporter("OTEL_METRICS_EXPORTER"), | ||
| 316 | logs: exporter("OTEL_LOGS_EXPORTER"), | ||
| 317 | }), | ||
| 266 | }).pipe( | 318 | }).pipe( |
| 267 | Effect.map(({ t3, spec, resource, generic, ...own }) => { | 319 | Effect.map(({ t3, spec, resource, generic, exporters, ...own }) => { |
| 268 | const disabled = t3.value ?? spec.value ?? false; | 320 | const disabled = t3.value ?? spec.value ?? false; |
| 269 | // The kill switch wins outright, so the signals say nothing once it is set. | 321 | // The kill switch wins outright, so the signals say nothing once it is set. |
| 270 | const signals = disabled | 322 | const signals = disabled |
| 271 | ? undefined | 323 | ? undefined |
| 272 | : { | 324 | : { |
| 273 | traces: signal("TRACES", own.traces, generic), | 325 | traces: signal("TRACES", exporters.traces, own.traces, generic), |
| 274 | metrics: signal("METRICS", own.metrics, generic), | 326 | metrics: signal("METRICS", exporters.metrics, own.metrics, generic), |
| 275 | logs: signal("LOGS", own.logs, generic), | 327 | logs: signal("LOGS", exporters.logs, own.logs, generic), |
| 276 | }; | 328 | }; |
| 277 | // A generic variable read by several signals warns once. | 329 | // A generic variable read by several signals warns once. |
| 278 | const used = new Set( | 330 | const used = new Set( |