| 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 | 118 | "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", |
| 119 | 119 | "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL", |
| 120 | 120 | "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL", |
| 121 | "OTEL_TRACES_EXPORTER", | |
| 122 | "OTEL_METRICS_EXPORTER", | |
| 123 | "OTEL_LOGS_EXPORTER", | |
| 121 | 124 | ] as const; |
| 122 | 125 | |
| 123 | 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 | 631 | OpenTelemetry specification and only `true` disables export, so `OTEL_SDK_DISABLED=1` does not. |
| 632 | 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 | 639 | ### What Is Instrumented Today |
| 635 | 640 | |
| 636 | 641 | Current high-value span and metric boundaries include: |
packages/shared/src/otelEnvironment.test.ts+86| ... | ... | @@ -204,6 +204,73 @@ describe("OtelEnvironment", () => { |
| 204 | 204 | logs: "Unset", |
| 205 | 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 | 274 | ])("$name", ({ env, traces, metrics, logs, warnings }) => |
| 208 | 275 | Effect.gen(function* () { |
| 209 | 276 | const resolved = yield* load(env); |
| ... | ... | @@ -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 | 485 | describe("layerResourceAttributes", () => { |
| 400 | 486 | it.effect.each([ |
| 401 | 487 | { name: "a list that does not decode", raw: "team=%zz", attributes: [] }, |
packages/shared/src/otelEnvironment.ts+61-9| ... | ... | @@ -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 | 3 | * shared by the server and the desktop main process so both agree on what |
| 4 | 4 | * turns export off and where it goes. |
| 5 | 5 | * |
| ... | ... | @@ -23,8 +23,9 @@ type OtlpSignalName = "TRACES" | "METRICS" | "LOGS"; |
| 23 | 23 | |
| 24 | 24 | /** |
| 25 | 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 | |
| 27 | * exported nowhere rather than to the bootstrap or Settings collector. | |
| 26 | * claimed with an endpoint, protocol, or headers that do not read, or turned | |
| 27 | * off with `OTEL_<SIGNAL>_EXPORTER=none`, so it is exported nowhere rather | |
| 28 | * than to the bootstrap or Settings collector. | |
| 28 | 29 | */ |
| 29 | 30 | export type OtelSignal = Data.TaggedEnum<{ |
| 30 | 31 | Unset: {}; |
| ... | ... | @@ -179,6 +180,38 @@ const headers = (name: string) => |
| 179 | 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 | 215 | interface Settings { |
| 183 | 216 | readonly endpoint: Setting<URL>; |
| 184 | 217 | readonly protocol: Setting<OtlpProtocol>; |
| ... | ... | @@ -215,9 +248,23 @@ interface ResolvedSignal { |
| 215 | 248 | /** |
| 216 | 249 | * A signal whose endpoint, protocol, or headers do not read is not exported |
| 217 | 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 | 268 | const ownEndpoint = isClaimed(own.endpoint); |
| 222 | 269 | const endpoint = ownEndpoint ? own.endpoint : generic.endpoint; |
| 223 | 270 | if (endpoint.value === undefined) { |
| ... | ... | @@ -263,16 +310,21 @@ export const load: Effect.Effect<OtelEnvironment> = Config.all({ |
| 263 | 310 | traces: settings("OTEL_EXPORTER_OTLP_TRACES_"), |
| 264 | 311 | metrics: settings("OTEL_EXPORTER_OTLP_METRICS_"), |
| 265 | 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 | 318 | }).pipe( |
| 267 | Effect.map(({ t3, spec, resource, generic, ...own }) => { | |
| 319 | Effect.map(({ t3, spec, resource, generic, exporters, ...own }) => { | |
| 268 | 320 | const disabled = t3.value ?? spec.value ?? false; |
| 269 | 321 | // The kill switch wins outright, so the signals say nothing once it is set. |
| 270 | 322 | const signals = disabled |
| 271 | 323 | ? undefined |
| 272 | 324 | : { |
| 273 | traces: signal("TRACES", own.traces, generic), | |
| 274 | metrics: signal("METRICS", own.metrics, generic), | |
| 275 | logs: signal("LOGS", own.logs, generic), | |
| 325 | traces: signal("TRACES", exporters.traces, own.traces, generic), | |
| 326 | metrics: signal("METRICS", exporters.metrics, own.metrics, generic), | |
| 327 | logs: signal("LOGS", exporters.logs, own.logs, generic), | |
| 276 | 328 | }; |
| 277 | 329 | // A generic variable read by several signals warns once. |
| 278 | 330 | const used = new Set( |