OtlpProxy unstable
Proxies OTLP HTTP exports from browsers through an application's own HTTP server to an OpenTelemetry Collector or vendor endpoint.
A browser exporting OTLP directly would need the vendor credentials in its
bundle and a cross-origin collector. OtlpProxy mounts POST /v1/traces,
/v1/logs, and /v1/metrics on an HttpRouter, adds the configured
headers on the server, and forwards the payload unchanged. The browser can
then use Otlp.layer({ baseUrl: "/otlp" }) against its own origin.
Details
- The upstream URL comes only from configuration. The request path and query string never reach it.
- Only
application/jsonandapplication/x-protobufbodies are accepted; other content types get415. Payloads are not decoded or validated. - Only
content-typeandcontent-encodingare forwarded from the client. Configuredheadersare applied last, so a client cannot override them. - Bodies are buffered up to
maxBodySizeand rejected with413beyond it. - Upstream requests send
accept-encoding: identity, and the client'saccept-encodingis never forwarded. - The upstream status and body are returned with only the
content-typeandretry-afterheaders, so client SDK back-off keeps working and upstream cookies are dropped. Transport failures return502and timeouts504. The proxy does not retry or batch.
Gotchas
- Anyone who can reach the routes can send telemetry with the configured credentials. Compose authentication, CORS, and rate limiting around the layer as the application requires.
- The response body is returned as the
HttpClientprovides it, without acontent-encodingheader. Settingaccept-encodinginheaderscan invalidate this, as can an upstream that compresses despiteaccept-encoding: identity: clients that do not decode responses, such asNodeHttpClient.makeUndici, then return compressed bytes. - Requests to the proxy routes are traced by the HTTP server like any other
route. To suppress those spans, provide
HttpMiddleware.TracerDisabledWhenwith a predicate matching the routes, or useHttpMiddleware.layerTracerDisabledForUrls, which matches exact URLs, so list each route.
Constructors
Creates a record of request handlers, one for each OTLP signal, that forward to the configured upstream.
When to use
Use to mount the proxy on routes or a router other than the ones layer
registers, such as an HttpApi endpoint.
Details
Each signal is sent to urls[signal] when set, otherwise to
/v1/<signal> below baseUrl. maxBodySize defaults to the
HttpIncomingMessage.MaxBodySize reference, then 10 MiB. timeout defaults
to 10 seconds.
See
- layer for mounting the handlers on an
HttpRouter.
Signature
declare function make(options: { readonly baseUrl: string; readonly headers?: Input; readonly maxBodySize?: Input; readonly timeout?: Input; readonly urls?: Partial<Record<Signal, string>>;}): Effect<Record<Signal, Effect<HttpServerResponse, never, HttpServerRequest>>, never, HttpClient>Layers
Creates a layer that mounts the OTLP proxy routes on an HttpRouter.
Details
Registers POST <prefix>/v1/<signal> for each of signals, which defaults
to traces, logs, and metrics. Without a prefix the routes are mounted at
the router root.
See
- layerFromConfig for reading the upstream from OpenTelemetry environment variables.
Signature
declare function layer(options: { readonly baseUrl: string; readonly headers?: Input; readonly maxBodySize?: Input; readonly prefix?: string; readonly signals?: readonly Array<Signal>; readonly timeout?: Input; readonly urls?: Partial<Record<Signal, string>>;}): Layer<never, never, HttpClient | HttpRouter>Example Proxying browser telemetry to a vendor
import { Layer } from "effect"import { FetchHttpClient } from "effect/http"import { OtlpProxy } from "effect/observability"
const OtlpProxyLayer = OtlpProxy.layer({ baseUrl: "https://otlp.vendor.example", headers: { authorization: "Bearer server-side-key" }, prefix: "/otlp"}).pipe(Layer.provide(FetchHttpClient.layer))
Layer.isLayer(OtlpProxyLayer) // => truelayerFromConfig
unstableCreates a layer that mounts the OTLP proxy routes using the upstream from OpenTelemetry environment configuration.
Details
Each signal is sent to OTEL_EXPORTER_OTLP_<SIGNAL>_ENDPOINT, falling back
to /v1/<signal> below OTEL_EXPORTER_OTLP_ENDPOINT, with headers from
OTEL_EXPORTER_OTLP_<SIGNAL>_HEADERS or OTEL_EXPORTER_OTLP_HEADERS. The
headers option replaces the configured headers. Signals without an
endpoint get no route.
See
- layer for configuring the upstream directly.
Signature
declare function layerFromConfig(options?: { readonly headers?: Input; readonly maxBodySize?: Input; readonly prefix?: string; readonly signals?: readonly Array<Signal>; readonly timeout?: Input;}): Layer<never, never, HttpClient | HttpRouter>