Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 140 additions & 8 deletions apps/desktop/src/app/DesktopObservability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ const collectorLayer = (requests: Array<ExportedRequest>) =>
),
);

// A developer's own OTEL_* variables would otherwise pick the endpoints.
const emptyEnv = ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} }));

const encodeObservabilitySettingsFile = Schema.encodeSync(
Schema.fromJsonString(
Schema.Struct({ observability: Schema.Record(Schema.String, Schema.String) }),
Expand Down Expand Up @@ -182,7 +185,7 @@ describe("DesktopObservability", () => {
assert.isFalse(yield* fileSystem.exists(logPath));
}).pipe(
Effect.scoped,
Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici)),
Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, emptyEnv)),
),
);

Expand Down Expand Up @@ -260,7 +263,7 @@ describe("DesktopObservability", () => {
);
}).pipe(
Effect.scoped,
Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici)),
Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, emptyEnv)),
),
);

Expand Down Expand Up @@ -300,7 +303,7 @@ describe("DesktopObservability", () => {
assert.equal(records.at(-1)?.annotations.details, "code=1");
}).pipe(
Effect.scoped,
Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici)),
Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, emptyEnv)),
),
);

Expand Down Expand Up @@ -344,7 +347,7 @@ describe("DesktopObservability", () => {
assert.isFalse(text.includes("y"));
}).pipe(
Effect.scoped,
Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici)),
Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, emptyEnv)),
),
);

Expand Down Expand Up @@ -378,7 +381,7 @@ describe("DesktopObservability", () => {
assert.equal(lines.length, 258);
}).pipe(
Effect.scoped,
Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici)),
Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici, emptyEnv)),
),
);

Expand Down Expand Up @@ -424,7 +427,136 @@ describe("DesktopObservability", () => {
assert.lengthOf(record?.events ?? [], 0);
}).pipe(
Effect.scoped,
Effect.provide(Layer.mergeAll(NodeServices.layer, collectorLayer(requests))),
Effect.provide(Layer.mergeAll(NodeServices.layer, collectorLayer(requests), emptyEnv)),
);
});

it.effect("exports to an OTEL endpoint over Settings, with its own headers and protocol", () => {
const requests: Array<ExportedRequest> = [];
return Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-observability-test-",
});
const environmentLayer = makeEnvironmentLayer(baseDir, true, {
T3CODE_OTLP_HEADERS: "x-scope=desktop",
});
yield* writeObservabilitySettings(environmentLayer, {
otlpLogsUrl: "https://settings.example.com/v1/logs",
});

yield* Effect.scoped(
Effect.logInfo("desktop otel export").pipe(
Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),
),
);

assert.lengthOf(requests, 1);
const [request] = requests;
assert.strictEqual(request?.url, "https://collector.example.com/v1/logs");
assert.strictEqual(request?.headers["x-otel"], "desktop");
assert.strictEqual(request?.headers["x-scope"], undefined);
assert.strictEqual(request?.headers["content-type"], "application/json");
}).pipe(
Effect.scoped,
Effect.provide(
Layer.mergeAll(
NodeServices.layer,
collectorLayer(requests),
ConfigProvider.layer(
ConfigProvider.fromEnv({
env: {
OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector.example.com",
OTEL_EXPORTER_OTLP_HEADERS: "x-otel=desktop",
OTEL_EXPORTER_OTLP_LOGS_PROTOCOL: "http/json",
},
}),
),
),
),
);
});

it.effect("keeps its service name while OTEL resource attributes add dimensions", () => {
const requests: Array<ExportedRequest> = [];
return Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-observability-test-",
});
const environmentLayer = makeEnvironmentLayer(baseDir, true, {
T3CODE_OTLP_LOGS_URL: "https://collector.example.com/v1/logs",
});

yield* Effect.scoped(
Effect.logInfo("desktop service name").pipe(
Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),
),
);

assert.lengthOf(requests, 1);
const body = requests[0]?.body ?? "";
assert.include(body, '"stringValue":"t3code-desktop"');
assert.include(body, "deployment.environment.name");
assert.include(body, '"key":"service.namespace","value":{"stringValue":"t3code"}');
assert.notInclude(body, "renamed");
}).pipe(
Effect.scoped,
Effect.provide(
Layer.mergeAll(
NodeServices.layer,
collectorLayer(requests),
ConfigProvider.layer(
ConfigProvider.fromEnv({
env: {
OTEL_SERVICE_NAME: "renamed",
OTEL_RESOURCE_ATTRIBUTES:
"service.name=renamed,service.namespace=renamed,deployment.environment.name=development",
},
}),
),
),
),
);
});

it.effect("exports nothing to Settings for logs an unusable OTEL endpoint claimed", () => {
const requests: Array<ExportedRequest> = [];
return Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-observability-test-",
});
const environmentLayer = makeEnvironmentLayer(baseDir, true, {
T3CODE_OTLP_HEADERS: "x-scope=desktop",
});
yield* writeObservabilitySettings(environmentLayer, {
otlpLogsUrl: "https://settings.example.com/v1/logs",
});

yield* Effect.scoped(
Effect.logInfo("desktop otel off").pipe(
Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),
),
);

assert.lengthOf(requests, 0);
}).pipe(
Effect.scoped,
Effect.provide(
Layer.mergeAll(
NodeServices.layer,
collectorLayer(requests),
ConfigProvider.layer(
ConfigProvider.fromEnv({
env: {
OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector.example.com",
OTEL_EXPORTER_OTLP_LOGS_PROTOCOL: "grpc",
},
}),
),
),
),
);
});

Expand Down Expand Up @@ -495,7 +627,7 @@ describe("DesktopObservability", () => {
);
}).pipe(
Effect.scoped,
Effect.provide(Layer.mergeAll(NodeServices.layer, collectorLayer(requests))),
Effect.provide(Layer.mergeAll(NodeServices.layer, collectorLayer(requests), emptyEnv)),
);
});

Expand All @@ -519,7 +651,7 @@ describe("DesktopObservability", () => {
assert.lengthOf(requests, 0);
}).pipe(
Effect.scoped,
Effect.provide(Layer.mergeAll(NodeServices.layer, collectorLayer(requests))),
Effect.provide(Layer.mergeAll(NodeServices.layer, collectorLayer(requests), emptyEnv)),
);
});

Expand Down
67 changes: 47 additions & 20 deletions apps/desktop/src/app/DesktopObservability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
makeLocalFileTracer,
makeTraceSink,
otlpSerializationLayer,
type SignalExport,
} from "@t3tools/shared/observability";
import * as OtelEnvironment from "@t3tools/shared/otelEnvironment";
import {
Expand Down Expand Up @@ -350,8 +351,10 @@ const readPersistedObservabilitySettings: Effect.Effect<
});

/**
* Settings is read once for every signal, so the main process cannot
* resolve traces against one revision of the file and logs against another.
* Resolved as the server resolves them, with persisted Settings as the
* fallback. Settings is read once for every signal, so the main process
* cannot resolve traces against one revision of the file and logs against
* another.
*/
const resolveOtlpEndpoints = Effect.gen(function* () {
const otel = yield* OtelEnvironment.load;
Expand All @@ -367,10 +370,30 @@ const resolveOtlpEndpoints = Effect.gen(function* () {

const environment = yield* DesktopEnvironment.DesktopEnvironment;
const persisted = yield* readPersistedObservabilitySettings;
const signalExport: SignalExport = {
protocol: environment.otlpProtocol,
headers: Option.getOrUndefined(environment.otlpHeaders),
exportIntervalMs: environment.otlpExportIntervalMs,
};
return {
traces: Option.getOrUndefined(environment.otlpTracesUrl) ?? persisted.otlpTracesUrl,
metrics: Option.getOrUndefined(environment.otlpMetricsUrl) ?? persisted.otlpMetricsUrl,
logs: Option.getOrUndefined(environment.otlpLogsUrl) ?? persisted.otlpLogsUrl,
traces: OtelEnvironment.resolveSignalEndpoint(
otel,
"traces",
{ url: Option.getOrUndefined(environment.otlpTracesUrl), export: signalExport },
persisted.otlpTracesUrl,
),
metrics: OtelEnvironment.resolveSignalEndpoint(
otel,
"metrics",
{ url: Option.getOrUndefined(environment.otlpMetricsUrl), export: signalExport },
persisted.otlpMetricsUrl,
),
logs: OtelEnvironment.resolveSignalEndpoint(
otel,
"logs",
{ url: Option.getOrUndefined(environment.otlpLogsUrl), export: signalExport },
persisted.otlpLogsUrl,
),
warnings: otel.warnings,
resourceAttributes: otel.resourceAttributes,
};
Expand Down Expand Up @@ -602,11 +625,10 @@ const telemetryLayer = Layer.unwrap(
Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const endpoints = yield* resolveOtlpEndpoints;
const headers = Option.getOrUndefined(environment.otlpHeaders);
const serializationLayer = otlpSerializationLayer(environment.otlpProtocol);
const resource = {
serviceName: "desktop",
serviceName: "t3code-desktop",
attributes: {
"service.namespace": "t3code",
"service.runtime": "desktop",
"service.mode": environment.isDevelopment ? "development" : "packaged",
},
Expand All @@ -629,14 +651,19 @@ const telemetryLayer = Layer.unwrap(
: [
Logger.consolePretty(),
OtlpLogger.make({
url: endpoints.logs,
exportInterval: `${environment.otlpExportIntervalMs} millis`,
headers,
url: endpoints.logs.url,
exportInterval: `${endpoints.logs.export.exportIntervalMs} millis`,
headers: endpoints.logs.export.headers,
resource,
}),
],
{ mergeWithExisting: false },
).pipe(Layer.provide(OtlpExporter.layerFlusher), Layer.provide(serializationLayer));
).pipe(
Layer.provide(OtlpExporter.layerFlusher),
Layer.provide(
otlpSerializationLayer(endpoints.logs?.export.protocol ?? environment.otlpProtocol),
),
);

const tracerLayer = Layer.unwrap(
Effect.gen(function* () {
Expand All @@ -651,11 +678,11 @@ const telemetryLayer = Layer.unwrap(
endpoints.traces === undefined
? undefined
: yield* OtlpTracer.make({
url: endpoints.traces,
exportInterval: `${environment.otlpExportIntervalMs} millis`,
headers,
url: endpoints.traces.url,
exportInterval: `${endpoints.traces.export.exportIntervalMs} millis`,
headers: endpoints.traces.export.headers,
resource,
}).pipe(Effect.provide(serializationLayer));
}).pipe(Effect.provide(otlpSerializationLayer(endpoints.traces.export.protocol)));
const tracer = yield* makeLocalFileTracer({
filePath: tracePath,
maxBytes: DESKTOP_LOG_FILE_MAX_BYTES,
Expand All @@ -679,11 +706,11 @@ const telemetryLayer = Layer.unwrap(
// endpoints.metrics === undefined
// ? Layer.empty
// : OtlpMetrics.layer({
// url: endpoints.metrics,
// exportInterval: `${environment.otlpExportIntervalMs} millis`,
// headers,
// url: endpoints.metrics.url,
// exportInterval: `${endpoints.metrics.export.exportIntervalMs} millis`,
// headers: endpoints.metrics.export.headers,
// resource,
// }).pipe(Layer.provide(serializationLayer));
// }).pipe(Layer.provide(otlpSerializationLayer(endpoints.metrics.export.protocol)));

// Logged once the loggers above are installed, so the warnings use them.
const otelWarningsLayer = Layer.effectDiscard(
Expand Down
Loading
Loading