Skip to content

@memhtml/telemetry

@memhtml/telemetry: the opt-in trace exporter. It sends the spans memhtml already records to an OpenTelemetry collector over OTLP HTTP, and it does nothing unless OTEL_EXPORTER_OTLP_ENDPOINT is set.

The package exports one function and two interfaces. layerTelemetry takes a TelemetryOptions and returns an Effect layer that a runtime assembly provides. The CLI provides it with the service name memhtml-cli (apps/cli/src/run.ts), and the MCP server with memhtml-mcp (apps/mcp/src/server.ts).

With OTEL_EXPORTER_OTLP_ENDPOINT unset or blank, the layer is Layer.empty and no socket is opened.

With the variable set, the layer replaces Effect’s tracer with Effect’s own OtlpTracer, which batches spans and posts them as JSON to <endpoint>/v1/traces, with trailing slashes on the endpoint dropped. The service.name resource attribute is OTEL_SERVICE_NAME when that variable is set and not blank, and the assembly’s serviceName otherwise.

layerTelemetry reads OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_SERVICE_NAME when it is called, from options.env when given and process.env otherwise. TelemetryModules names the two Effect modules the exporter needs, effect/http and effect/observability. This package imports them only when the endpoint is set, through a loader that options.load replaces so a test can check that the disabled path never calls it.

A failed export does not fail the command or change the envelope on stdout, the exit code or the MCP stream. The exporter retries transient failures, then stops exporting for a while. The first failed connection to the collector logs one warning on stderr, and an error status from the collector logs none.

The package is one module, src/index.ts, published as the import path @memhtml/telemetry, so this page is its whole exported surface.

The two Effect modules the exporter needs, imported here only when the endpoint is set.

readonly http: __module;

The effect/http module, for the fetch-backed HTTP client the exporter posts spans with.

readonly observability: __module;

The effect/observability module, for OtlpTracer and its JSON serialization.


What a runtime assembly passes to layerTelemetry.

readonly env?: Record<string, string | undefined>;

Where the endpoint and OTEL_SERVICE_NAME are read, defaulting to process.env, when the factory is CALLED rather than through effect/Config. Config’s default provider is built once per process and cached (hookConfigProvider in apps/cli/src/hook.ts records the probe), and this factory runs once per invocation, so either variable set by a test after an earlier read must still be seen.

readonly load?: () => Promise<TelemetryModules>;

The module loader, injectable so a test can PROVE the disabled path loads nothing: the acceptance bar is not “no spans exported” but “no exporter code even evaluated”, and only a loader spy can tell those apart.

Promise<TelemetryModules>

readonly serviceName: string;

The service.name resource attribute when OTEL_SERVICE_NAME is unset or blank: which of the runtime assemblies this process is (memhtml-cli, memhtml-mcp), because one collector typically receives both and the operator’s first question is which process a trace came from.

function layerTelemetry(options): Layer<never>;

The tracer layer for one runtime assembly.

OTEL_EXPORTER_OTLP_ENDPOINT unset or blank returns Layer.empty: the program runs as it would without this package, with no exporter module loaded and no socket opened. Set, the layer overrides effect’s tracer reference with a batching OTLP HTTP exporter aimed at <endpoint>/v1/traces (the standard per-signal path; a trailing slash on the endpoint is tolerated because both spellings arrive from shell profiles).

A failed export never fails a command. The exporter retries transient failures, then disables itself and drops the batch, and the program’s error channel never sees it, so the envelope, the exit code, and the MCP stream are exactly what they would have been. The final batch of a short-lived process is flushed when the layer’s scope closes, bounded by the exporter’s own shutdown timeout so a dead collector delays exit by seconds, never hangs it.

TelemetryOptions

Layer<never>