@smthrs/observability 0.34.0 → 1.0.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/LICENSE +1 -1
- package/README.md +84 -0
- package/dist/cjs/BrowserOtel.d.ts +37 -0
- package/dist/cjs/BrowserOtel.d.ts.map +1 -0
- package/dist/cjs/BrowserOtel.js +47 -0
- package/dist/cjs/BrowserOtel.js.map +7 -0
- package/dist/cjs/Endpoint.d.ts +70 -0
- package/dist/cjs/Endpoint.d.ts.map +1 -0
- package/dist/cjs/Endpoint.js +85 -0
- package/dist/cjs/Endpoint.js.map +7 -0
- package/dist/cjs/JournalLogger.d.ts +232 -0
- package/dist/cjs/JournalLogger.d.ts.map +1 -0
- package/dist/cjs/JournalLogger.js +388 -0
- package/dist/cjs/JournalLogger.js.map +7 -0
- package/dist/cjs/Logger.d.ts +55 -0
- package/dist/cjs/Logger.d.ts.map +1 -0
- package/dist/cjs/Logger.js +72 -0
- package/dist/cjs/Logger.js.map +7 -0
- package/dist/cjs/Metric.d.ts +67 -0
- package/dist/cjs/Metric.d.ts.map +1 -0
- package/dist/cjs/Metric.js +68 -0
- package/dist/cjs/Metric.js.map +7 -0
- package/dist/cjs/NodeOtel.d.ts +42 -0
- package/dist/cjs/NodeOtel.d.ts.map +1 -0
- package/dist/cjs/NodeOtel.js +78 -0
- package/dist/cjs/NodeOtel.js.map +7 -0
- package/dist/cjs/Otel.d.ts +62 -0
- package/dist/cjs/Otel.d.ts.map +1 -0
- package/dist/cjs/Otel.js +75 -0
- package/dist/cjs/Otel.js.map +7 -0
- package/dist/cjs/Otlp.d.ts +200 -0
- package/dist/cjs/Otlp.d.ts.map +1 -0
- package/dist/cjs/Otlp.js +235 -0
- package/dist/cjs/Otlp.js.map +7 -0
- package/dist/cjs/Resource.d.ts +162 -0
- package/dist/cjs/Resource.d.ts.map +1 -0
- package/dist/cjs/Resource.js +186 -0
- package/dist/cjs/Resource.js.map +7 -0
- package/dist/cjs/index.d.ts +61 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +47 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/schemaIssuePath.d.ts +31 -0
- package/dist/cjs/internal/schemaIssuePath.d.ts.map +1 -0
- package/dist/cjs/internal/schemaIssuePath.js +49 -0
- package/dist/cjs/internal/schemaIssuePath.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/BrowserOtel.d.ts +37 -0
- package/dist/esm/BrowserOtel.d.ts.map +1 -0
- package/dist/esm/BrowserOtel.js +24 -0
- package/dist/esm/BrowserOtel.js.map +1 -0
- package/dist/esm/Endpoint.d.ts +70 -0
- package/dist/esm/Endpoint.d.ts.map +1 -0
- package/dist/esm/Endpoint.js +110 -0
- package/dist/esm/Endpoint.js.map +1 -0
- package/dist/esm/JournalLogger.d.ts +232 -0
- package/dist/esm/JournalLogger.d.ts.map +1 -0
- package/dist/esm/JournalLogger.js +482 -0
- package/dist/esm/JournalLogger.js.map +1 -0
- package/dist/esm/Logger.d.ts +55 -0
- package/dist/esm/Logger.d.ts.map +1 -0
- package/dist/esm/Logger.js +60 -0
- package/dist/esm/Logger.js.map +1 -0
- package/dist/esm/Metric.d.ts +67 -0
- package/dist/esm/Metric.d.ts.map +1 -0
- package/dist/esm/Metric.js +80 -0
- package/dist/esm/Metric.js.map +1 -0
- package/dist/esm/NodeOtel.d.ts +42 -0
- package/dist/esm/NodeOtel.d.ts.map +1 -0
- package/dist/esm/NodeOtel.js +56 -0
- package/dist/esm/NodeOtel.js.map +1 -0
- package/dist/esm/Otel.d.ts +62 -0
- package/dist/esm/Otel.d.ts.map +1 -0
- package/dist/esm/Otel.js +51 -0
- package/dist/esm/Otel.js.map +1 -0
- package/dist/esm/Otlp.d.ts +200 -0
- package/dist/esm/Otlp.d.ts.map +1 -0
- package/dist/esm/Otlp.js +350 -0
- package/dist/esm/Otlp.js.map +1 -0
- package/dist/esm/Resource.d.ts +162 -0
- package/dist/esm/Resource.d.ts.map +1 -0
- package/dist/esm/Resource.js +237 -0
- package/dist/esm/Resource.js.map +1 -0
- package/dist/esm/index.d.ts +61 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +61 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/schemaIssuePath.d.ts +31 -0
- package/dist/esm/internal/schemaIssuePath.d.ts.map +1 -0
- package/dist/esm/internal/schemaIssuePath.js +59 -0
- package/dist/esm/internal/schemaIssuePath.js.map +1 -0
- package/docs/README.md +162 -0
- package/docs/api.md +768 -0
- package/docs/concepts/instrumentation-and-export.md +68 -0
- package/docs/concepts/layer-map.md +72 -0
- package/docs/concepts/validated-acquisition.md +105 -0
- package/docs/guides/export-to-a-collector.md +167 -0
- package/docs/guides/forward-logs-to-the-journal.md +226 -0
- package/docs/guides/install-a-logger.md +111 -0
- package/docs/guides/read-runtime-metrics.md +83 -0
- package/docs/guides/testing.md +134 -0
- package/docs/guides/wire-an-opentelemetry-sdk.md +220 -0
- package/docs/installation.md +98 -0
- package/docs/quickstart.md +145 -0
- package/docs/troubleshooting.md +168 -0
- package/package.json +221 -32
- package/src/BrowserOtel.ts +51 -0
- package/src/Endpoint.ts +131 -0
- package/src/JournalLogger.ts +596 -0
- package/src/Logger.ts +106 -0
- package/src/Metric.ts +88 -0
- package/src/NodeOtel.ts +90 -0
- package/src/Otel.ts +117 -0
- package/src/Otlp.ts +477 -0
- package/src/Resource.ts +324 -0
- package/src/index.ts +67 -0
- package/src/internal/schemaIssuePath.ts +65 -0
- package/docker-compose.otel.yml +0 -104
- package/grafana/dashboards/smithers-dashboard.json +0 -453
- package/grafana/provisioning/dashboards/dashboards.yml +0 -12
- package/grafana/provisioning/datasources/datasources.yml +0 -49
- package/loki/loki-config.yaml +0 -35
- package/otel-collector-config.yml +0 -38
- package/prometheus/prometheus.yml +0 -8
- package/src/MetricName.ts +0 -1
- package/src/MetricsServiceLive.js +0 -9
- package/src/README.md +0 -28
- package/src/ResolvedSmithersObservabilityOptions.ts +0 -12
- package/src/SmithersEvent.ts +0 -798
- package/src/SmithersLogFormat.ts +0 -1
- package/src/SmithersMetricDefinition.ts +0 -15
- package/src/SmithersMetricType.ts +0 -1
- package/src/SmithersMetricUnit.ts +0 -1
- package/src/SmithersObservability.js +0 -15
- package/src/SmithersObservabilityOptions.ts +0 -12
- package/src/SmithersObservabilityService.ts +0 -13
- package/src/_coreCorrelation/CorrelationContext.ts +0 -10
- package/src/_coreCorrelation/CorrelationContextLive.js +0 -11
- package/src/_coreCorrelation/CorrelationContextService.js +0 -15
- package/src/_coreCorrelation/CorrelationContextServiceShape.ts +0 -12
- package/src/_coreCorrelation/CorrelationPatch.ts +0 -3
- package/src/_coreCorrelation/README.md +0 -20
- package/src/_coreCorrelation/_correlationStorage.js +0 -4
- package/src/_coreCorrelation/correlationContextFiberRef.js +0 -5
- package/src/_coreCorrelation/correlationContextToLogAnnotations.js +0 -18
- package/src/_coreCorrelation/getCurrentCorrelationContext.js +0 -9
- package/src/_coreCorrelation/getCurrentCorrelationContextEffect.js +0 -11
- package/src/_coreCorrelation/index.js +0 -14
- package/src/_coreCorrelation/mergeCorrelationContext.js +0 -51
- package/src/_coreCorrelation/runWithCorrelationContext.js +0 -14
- package/src/_coreCorrelation/updateCurrentCorrelationContext.js +0 -17
- package/src/_coreCorrelation/withCorrelationContext.js +0 -38
- package/src/_coreCorrelation/withCurrentCorrelationContext.js +0 -11
- package/src/_coreMetrics.js +0 -14
- package/src/_coreMetricsShape.ts +0 -44
- package/src/_corePrometheus.js +0 -91
- package/src/_corePrometheusShape.ts +0 -11
- package/src/_coreTracing.js +0 -132
- package/src/_coreTracingShape.ts +0 -17
- package/src/_otelLogBuilders.js +0 -102
- package/src/_sessionFileResolvers.js +0 -164
- package/src/_smithersSpanAttributeAliases.js +0 -19
- package/src/_smithersTraceSpanStorage.js +0 -3
- package/src/_traceEventNormalizers.js +0 -499
- package/src/_traceRedaction.js +0 -162
- package/src/agentSessionEventToOtelLogRecord.js +0 -55
- package/src/agentTrace.ts +0 -144
- package/src/agentTraceCapabilities.js +0 -151
- package/src/annotateSmithersTrace.js +0 -11
- package/src/canonicalTraceEventToOtelLogRecord.js +0 -55
- package/src/correlation.js +0 -56
- package/src/createSmithersObservabilityLayer.js +0 -59
- package/src/createSmithersOtelLayer.js +0 -23
- package/src/createSmithersRuntimeLayer.js +0 -2
- package/src/detectAgentFamily.js +0 -26
- package/src/detectCaptureMode.js +0 -29
- package/src/emitOtelLogRecord.js +0 -19
- package/src/getCurrentSmithersTraceAnnotations.js +0 -14
- package/src/getCurrentSmithersTraceSpan.js +0 -7
- package/src/index.d.ts +0 -1343
- package/src/index.js +0 -136
- package/src/kindPhase.js +0 -17
- package/src/logging.js +0 -248
- package/src/makeSmithersSpanAttributes.js +0 -20
- package/src/metrics/README.md +0 -22
- package/src/metrics/SmithersMetricDefinition.ts +0 -17
- package/src/metrics/SmithersMetricType.ts +0 -1
- package/src/metrics/SmithersMetricUnit.ts +0 -1
- package/src/metrics/_asyncExternalWaitCounts.js +0 -4
- package/src/metrics/_buckets.js +0 -37
- package/src/metrics/_incrementGauge.js +0 -11
- package/src/metrics/_processStartMs.js +0 -1
- package/src/metrics/activeNodes.js +0 -2
- package/src/metrics/activeRuns.js +0 -2
- package/src/metrics/agentActionsTotal.js +0 -2
- package/src/metrics/agentDurationMs.js +0 -3
- package/src/metrics/agentErrorsTotal.js +0 -2
- package/src/metrics/agentEventsTotal.js +0 -2
- package/src/metrics/agentInvocationsTotal.js +0 -2
- package/src/metrics/agentRetriesTotal.js +0 -2
- package/src/metrics/agentSessionsTotal.js +0 -2
- package/src/metrics/agentTokensTotal.js +0 -2
- package/src/metrics/alertDeliveriesAttempted.js +0 -2
- package/src/metrics/alertDeliveriesSuppressed.js +0 -2
- package/src/metrics/alertsAcknowledgedTotal.js +0 -2
- package/src/metrics/alertsActive.js +0 -2
- package/src/metrics/alertsEscalatedTotal.js +0 -2
- package/src/metrics/alertsFiredTotal.js +0 -2
- package/src/metrics/alertsReopenedTotal.js +0 -2
- package/src/metrics/alertsResolvedTotal.js +0 -2
- package/src/metrics/alertsSilencedTotal.js +0 -2
- package/src/metrics/approvalPending.js +0 -2
- package/src/metrics/approvalWaitDuration.js +0 -5
- package/src/metrics/approvalsDenied.js +0 -2
- package/src/metrics/approvalsGranted.js +0 -2
- package/src/metrics/approvalsRequested.js +0 -2
- package/src/metrics/attemptDuration.js +0 -3
- package/src/metrics/attentionBacklog.js +0 -2
- package/src/metrics/cacheHits.js +0 -2
- package/src/metrics/cacheMisses.js +0 -2
- package/src/metrics/dbQueryDuration.js +0 -3
- package/src/metrics/dbRetries.js +0 -2
- package/src/metrics/dbTransactionDuration.js +0 -3
- package/src/metrics/dbTransactionRetries.js +0 -2
- package/src/metrics/dbTransactionRollbacks.js +0 -2
- package/src/metrics/devtoolsActiveSubscribers.js +0 -2
- package/src/metrics/devtoolsBackpressureDisconnectTotal.js +0 -2
- package/src/metrics/devtoolsDeltaBuildMs.js +0 -3
- package/src/metrics/devtoolsEventBytes.js +0 -3
- package/src/metrics/devtoolsEventTotal.js +0 -2
- package/src/metrics/devtoolsSnapshotBuildMs.js +0 -5
- package/src/metrics/devtoolsSubscribeTotal.js +0 -2
- package/src/metrics/errorsTotal.js +0 -2
- package/src/metrics/eventsEmittedTotal.js +0 -2
- package/src/metrics/externalWaitAsyncPending.js +0 -2
- package/src/metrics/gatewayApprovalDecisionsTotal.js +0 -2
- package/src/metrics/gatewayAuthEventsTotal.js +0 -2
- package/src/metrics/gatewayConnectionsActive.js +0 -2
- package/src/metrics/gatewayConnectionsClosedTotal.js +0 -2
- package/src/metrics/gatewayConnectionsTotal.js +0 -2
- package/src/metrics/gatewayCronTriggersTotal.js +0 -2
- package/src/metrics/gatewayErrorsTotal.js +0 -2
- package/src/metrics/gatewayHeartbeatTicksTotal.js +0 -2
- package/src/metrics/gatewayMessagesReceivedTotal.js +0 -2
- package/src/metrics/gatewayMessagesSentTotal.js +0 -2
- package/src/metrics/gatewayRpcCallsTotal.js +0 -2
- package/src/metrics/gatewayRpcDuration.js +0 -3
- package/src/metrics/gatewayRunEventBackpressureDisconnectTotal.js +0 -4
- package/src/metrics/gatewayRunsCompletedTotal.js +0 -2
- package/src/metrics/gatewayRunsStartedTotal.js +0 -2
- package/src/metrics/gatewaySignalsTotal.js +0 -2
- package/src/metrics/gatewayWebhooksReceivedTotal.js +0 -2
- package/src/metrics/gatewayWebhooksRejectedTotal.js +0 -2
- package/src/metrics/gatewayWebhooksVerifiedTotal.js +0 -2
- package/src/metrics/heartbeatDataSizeBytes.js +0 -5
- package/src/metrics/heartbeatIntervalMs.js +0 -3
- package/src/metrics/hotReloadDuration.js +0 -3
- package/src/metrics/hotReloadFailures.js +0 -2
- package/src/metrics/hotReloads.js +0 -2
- package/src/metrics/httpRequestDuration.js +0 -3
- package/src/metrics/httpRequests.js +0 -2
- package/src/metrics/index.d.ts +0 -1185
- package/src/metrics/index.js +0 -168
- package/src/metrics/memoryFactReads.js +0 -3
- package/src/metrics/memoryFactWrites.js +0 -3
- package/src/metrics/memoryMessageSaves.js +0 -3
- package/src/metrics/memoryRecallDuration.js +0 -11
- package/src/metrics/memoryRecallQueries.js +0 -3
- package/src/metrics/metricsServiceAdapter.js +0 -181
- package/src/metrics/nodeDuration.js +0 -3
- package/src/metrics/nodeRetriesTotal.js +0 -2
- package/src/metrics/nodesFailed.js +0 -2
- package/src/metrics/nodesFinished.js +0 -2
- package/src/metrics/nodesStarted.js +0 -2
- package/src/metrics/openApiToolCallErrorsTotal.js +0 -3
- package/src/metrics/openApiToolCallsTotal.js +0 -3
- package/src/metrics/openApiToolDuration.js +0 -9
- package/src/metrics/processHeapUsedBytes.js +0 -2
- package/src/metrics/processMemoryRssBytes.js +0 -2
- package/src/metrics/processUptimeSeconds.js +0 -2
- package/src/metrics/promptSizeBytes.js +0 -3
- package/src/metrics/replaysStarted.js +0 -3
- package/src/metrics/responseSizeBytes.js +0 -3
- package/src/metrics/rewindDurationMs.js +0 -3
- package/src/metrics/rewindFramesDeleted.js +0 -3
- package/src/metrics/rewindRollbackTotal.js +0 -2
- package/src/metrics/rewindSandboxesReverted.js +0 -3
- package/src/metrics/rewindTotal.js +0 -2
- package/src/metrics/runDuration.js +0 -3
- package/src/metrics/runForksCreated.js +0 -3
- package/src/metrics/runsAncestryDepth.js +0 -3
- package/src/metrics/runsCancelledTotal.js +0 -2
- package/src/metrics/runsCarriedStateBytes.js +0 -5
- package/src/metrics/runsContinuedTotal.js +0 -2
- package/src/metrics/runsFailedTotal.js +0 -2
- package/src/metrics/runsFinishedTotal.js +0 -2
- package/src/metrics/runsResumedTotal.js +0 -2
- package/src/metrics/runsTotal.js +0 -2
- package/src/metrics/sandboxActive.js +0 -2
- package/src/metrics/sandboxBundleSizeBytes.js +0 -5
- package/src/metrics/sandboxCompletedTotal.js +0 -2
- package/src/metrics/sandboxCreatedTotal.js +0 -2
- package/src/metrics/sandboxDurationMs.js +0 -3
- package/src/metrics/sandboxPatchCount.js +0 -3
- package/src/metrics/sandboxTransportDurationMs.js +0 -5
- package/src/metrics/schedulerConcurrencyUtilization.js +0 -2
- package/src/metrics/schedulerQueueDepth.js +0 -2
- package/src/metrics/schedulerWaitDuration.js +0 -5
- package/src/metrics/scorerDuration.js +0 -9
- package/src/metrics/scorerEventsFailed.js +0 -2
- package/src/metrics/scorerEventsFinished.js +0 -2
- package/src/metrics/scorerEventsStarted.js +0 -2
- package/src/metrics/scorersFailed.js +0 -3
- package/src/metrics/scorersFinished.js +0 -3
- package/src/metrics/scorersStarted.js +0 -3
- package/src/metrics/smithersMetricCatalog.js +0 -893
- package/src/metrics/smithersMetricCatalogByKey.js +0 -2
- package/src/metrics/smithersMetricCatalogByName.js +0 -2
- package/src/metrics/smithersMetricCatalogByPrometheusName.js +0 -4
- package/src/metrics/snapshotDuration.js +0 -9
- package/src/metrics/snapshotsCaptured.js +0 -3
- package/src/metrics/supervisorPollDuration.js +0 -5
- package/src/metrics/supervisorPollsTotal.js +0 -2
- package/src/metrics/supervisorResumeLag.js +0 -5
- package/src/metrics/supervisorResumedTotal.js +0 -2
- package/src/metrics/supervisorSkippedTotal.js +0 -2
- package/src/metrics/supervisorStaleDetected.js +0 -2
- package/src/metrics/taskHeartbeatTimeoutTotal.js +0 -2
- package/src/metrics/taskHeartbeatsTotal.js +0 -2
- package/src/metrics/timerDelayDuration.js +0 -3
- package/src/metrics/timersCancelled.js +0 -2
- package/src/metrics/timersCreated.js +0 -2
- package/src/metrics/timersFired.js +0 -2
- package/src/metrics/timersPending.js +0 -2
- package/src/metrics/toPrometheusMetricName.js +0 -8
- package/src/metrics/tokensCacheReadTotal.js +0 -2
- package/src/metrics/tokensCacheWriteTotal.js +0 -2
- package/src/metrics/tokensContextWindowBucketTotal.js +0 -2
- package/src/metrics/tokensContextWindowPerCall.js +0 -5
- package/src/metrics/tokensInputPerCall.js +0 -3
- package/src/metrics/tokensInputTotal.js +0 -2
- package/src/metrics/tokensOutputPerCall.js +0 -3
- package/src/metrics/tokensOutputTotal.js +0 -2
- package/src/metrics/tokensReasoningTotal.js +0 -2
- package/src/metrics/toolCallErrorsTotal.js +0 -2
- package/src/metrics/toolCallsTotal.js +0 -2
- package/src/metrics/toolDuration.js +0 -3
- package/src/metrics/toolOutputTruncatedTotal.js +0 -2
- package/src/metrics/trackEvent.js +0 -608
- package/src/metrics/updateAsyncExternalWaitPending.js +0 -18
- package/src/metrics/updateProcessMetrics.js +0 -20
- package/src/metrics/vcsDuration.js +0 -3
- package/src/normalizeStructuredEvent.js +0 -15
- package/src/prometheusContentType.js +0 -1
- package/src/renderPrometheusMetrics.js +0 -212
- package/src/resolveAgentTraceCapabilities.js +0 -59
- package/src/resolveSmithersObservabilityOptions.js +0 -109
- package/src/smithersMetrics.js +0 -2
- package/src/smithersSpanNames.js +0 -6
- package/src/unsupportedKindsForCapabilities.js +0 -32
- package/src/withSmithersSpan.js +0 -17
- package/tempo/tempo.yml +0 -17
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Instrumentation and export"
|
|
3
|
+
description: "Why the Smithers packages instrument themselves but export nothing, what that split buys, and the three independent ways to read the same run."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 1
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Instrumentation and export are two different jobs, and Smithers puts them in
|
|
9
|
+
different packages on purpose.
|
|
10
|
+
|
|
11
|
+
## Every other package is already instrumented
|
|
12
|
+
|
|
13
|
+
A flow execution opens spans without being asked. The flow lifecycle, the
|
|
14
|
+
engine dispatch, a run claim, a heartbeat, a journal write, down to individual
|
|
15
|
+
`sql.execute` statements: each is a span on Effect's own tracer. Alongside
|
|
16
|
+
them, each store package declares and updates its own metric handles on its hot
|
|
17
|
+
paths: `JournalMetrics` in [`@smthrs/journal`](/api/journal), `RunStoreMetrics`
|
|
18
|
+
in [`@smthrs/run-store`](/api/run-store), `CacheStoreMetrics` in
|
|
19
|
+
[`@smthrs/step-cache`](/api/step-cache), `ArtifactStoreMetrics` in
|
|
20
|
+
[`@smthrs/artifacts`](/api/artifacts), and `DatabaseMetrics` in
|
|
21
|
+
[`@smthrs/database`](/api/database).
|
|
22
|
+
|
|
23
|
+
None of those packages allocates an exporter. Effect's tracer and metric
|
|
24
|
+
registry are collection points; with no exporter installed they collect and
|
|
25
|
+
discard, at a cost close to zero.
|
|
26
|
+
|
|
27
|
+
## This package is the exporter
|
|
28
|
+
|
|
29
|
+
`Otlp.layerFetch` installs Effect's OTLP logger, metrics exporter, and tracer
|
|
30
|
+
against a collector endpoint, with a service identity filled in. That is the
|
|
31
|
+
whole wiring, and it is why telemetry is additive rather than invasive:
|
|
32
|
+
providing one layer turns on delivery for instrumentation that was already
|
|
33
|
+
there, and removing the line turns it off without touching a flow body.
|
|
34
|
+
|
|
35
|
+
The split also keeps the dependency in one place. Because `Otlp` composes only
|
|
36
|
+
what `effect` already ships, a package that wants a counter takes on no
|
|
37
|
+
OpenTelemetry dependency to get one, and the process pays for an SDK only if a
|
|
38
|
+
host chooses `NodeOtel` or `BrowserOtel`.
|
|
39
|
+
|
|
40
|
+
## The producer owns the meaning, the exporter owns the delivery
|
|
41
|
+
|
|
42
|
+
A metric handle belongs to the package that knows when to advance it. This
|
|
43
|
+
package holds only the four cross-package runtime signals nobody else owns,
|
|
44
|
+
and it holds them so the producers can share one identifier:
|
|
45
|
+
`flows_run_throughput` is updated in [`@smthrs/run-store`](/api/run-store),
|
|
46
|
+
`flows_seat_active` and `flows_quota_park` in [`@smthrs/agent`](/api/agent),
|
|
47
|
+
and `flows_observability_log_dropped` here. Step-cache counters stay in
|
|
48
|
+
[`@smthrs/step-cache`](/api/step-cache); this package does not duplicate them.
|
|
49
|
+
|
|
50
|
+
The consequence for a reader: a metric's name tells you which package to open
|
|
51
|
+
to learn what advances it, and this package tells you only how it leaves the
|
|
52
|
+
process.
|
|
53
|
+
|
|
54
|
+
## Three ways to read one run
|
|
55
|
+
|
|
56
|
+
Export is one read path, not the only one, and the other two need no collector:
|
|
57
|
+
|
|
58
|
+
1. **The OTLP trace.** Every span and series posts to your collector. Filter on
|
|
59
|
+
the `service.name` you passed to the layer.
|
|
60
|
+
2. **The durable journal.** `Journal.entries` reads a run's lifecycle events in
|
|
61
|
+
process, and `JournalLogger` puts the run's log records on that same
|
|
62
|
+
history. See [Forward logs to the run journal](../guides/forward-logs-to-the-journal.md).
|
|
63
|
+
3. **The metric handles.** Effect's `Metric.value` reads a counter in process,
|
|
64
|
+
with no exporter installed at all. See
|
|
65
|
+
[Read the runtime metrics](../guides/read-runtime-metrics.md).
|
|
66
|
+
|
|
67
|
+
The three tell the same story about the same run, which is what makes a
|
|
68
|
+
disagreement between them worth investigating.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "The layer map"
|
|
3
|
+
description: "The five telemetry layer builders this package ships, what each one allocates, and the rule that keeps the root entry point free of Node modules."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 2
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Five builders produce a telemetry layer, and they differ in one thing: how much
|
|
9
|
+
they allocate for you.
|
|
10
|
+
|
|
11
|
+
## The five builders
|
|
12
|
+
|
|
13
|
+
| Builder | Allocates | Reach for it when |
|
|
14
|
+
| ----------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
15
|
+
| `Otlp.layerFetch` | Effect's three OTLP exporters, plus the host's global `fetch` | You have a collector and no other OpenTelemetry requirement. This is the default. |
|
|
16
|
+
| `Otlp.layer` | Effect's three OTLP exporters. You provide the `HttpClient` | You need a specific HTTP client, such as Undici on Node. |
|
|
17
|
+
| `NodeOtel.layerOtel` | The Node OpenTelemetry SDK: three OTLP/HTTP exporters and their processors | Node code that must run through the OpenTelemetry SDK. |
|
|
18
|
+
| `BrowserOtel.layerOtel` | The web OpenTelemetry SDK around processors and readers you construct | Browser code that must run through the OpenTelemetry SDK. |
|
|
19
|
+
| `Otel.layerOtel` | Effect bridges; optional caller factories build providers | The application controls SDK provider construction. |
|
|
20
|
+
|
|
21
|
+
Each has an explicit do-nothing counterpart: `Otlp.layerNoop` and
|
|
22
|
+
`Otel.layerNoop` both provide nothing and export nothing, so wiring code
|
|
23
|
+
switches layers instead of branching around a missing one.
|
|
24
|
+
|
|
25
|
+
## Why `Otlp` is the default
|
|
26
|
+
|
|
27
|
+
`Otlp` composes only `effect/unstable/observability/Otlp`, so it adds no
|
|
28
|
+
OpenTelemetry SDK to the process, and it delivers over Effect's `HttpClient`
|
|
29
|
+
rather than a platform transport. That makes it the cheapest wiring and the
|
|
30
|
+
only one that runs unchanged in Node, in Bun, and in a browser.
|
|
31
|
+
|
|
32
|
+
Reach past it when something other than a collector endpoint is in play: an
|
|
33
|
+
existing OpenTelemetry instrumentation you must feed, a vendor exporter that is
|
|
34
|
+
not OTLP/HTTP JSON, or a provider a framework hands you.
|
|
35
|
+
|
|
36
|
+
## Why the root entry point stops at `Otel`
|
|
37
|
+
|
|
38
|
+
The root entry point exports `Otlp`, `Endpoint`, `JournalLogger`, `Logger`,
|
|
39
|
+
`Metric`, `Otel`, and `Resource`. It stops there because those seven resolve
|
|
40
|
+
nothing host-specific.
|
|
41
|
+
|
|
42
|
+
`NodeOtel` does. It reaches `@effect/opentelemetry/NodeSdk` and
|
|
43
|
+
`@opentelemetry/sdk-trace-node`, and the latter pulls
|
|
44
|
+
`@opentelemetry/context-async-hooks`, which imports the bare specifier
|
|
45
|
+
`async_hooks`. A browser bundler cannot resolve that, so re-exporting `NodeOtel`
|
|
46
|
+
from the root would break the root's browser bundle for every consumer,
|
|
47
|
+
including those that never wanted an SDK. `BrowserOtel` is kept beside it for
|
|
48
|
+
symmetry: a module that binds one host belongs on a subpath.
|
|
49
|
+
|
|
50
|
+
Nothing the root entry point reaches resolves a `node:` built-in, as an ESM
|
|
51
|
+
import or as a `require("node:...")` call, so a browser bundle of the root
|
|
52
|
+
carries no host module.
|
|
53
|
+
|
|
54
|
+
## `Resource` sits under all of them
|
|
55
|
+
|
|
56
|
+
Every builder decodes the same `Resource.Configuration`. `Otlp`, `NodeOtel`,
|
|
57
|
+
and `BrowserOtel` attach its attributes to all enabled signals. `Otel.layerOtel`
|
|
58
|
+
passes that validated resource to provider factories and the metric producer;
|
|
59
|
+
factories must use it when constructing tracer and logger providers.
|
|
60
|
+
|
|
61
|
+
Already-created providers retain their own resources. For those providers,
|
|
62
|
+
`Otel.layerOtel`'s resource option sets only the metric resource and tracer
|
|
63
|
+
instrumentation scope. Construct them with the intended resource before
|
|
64
|
+
injection to give all three signals the same service identity.
|
|
65
|
+
|
|
66
|
+
`Resource.layer` provides the resource on its own, for a composition that
|
|
67
|
+
assembles the rest itself.
|
|
68
|
+
|
|
69
|
+
`Otlp` is the one builder with defaults: `Otlp.defaultServiceName` is `flows`
|
|
70
|
+
and `Otlp.defaultServiceVersion` is this package's release version. Name your
|
|
71
|
+
own service in production, so a collector holding several Smithers processes
|
|
72
|
+
can tell them apart.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Validation at layer acquisition"
|
|
3
|
+
description: "Export failure is absorbed by design, so a misconfigured exporter looks exactly like a working one. Endpoints and resources are therefore decoded when the layer is built, and refused with typed errors."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 3
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
An exporter that cannot deliver must not take the application down with it. A
|
|
9
|
+
collector outage is not an application outage, so Effect's exporter absorbs
|
|
10
|
+
export failure: it retries a transient failure three times, then temporarily
|
|
11
|
+
disables delivery, and the effect that produced the span never learns any of
|
|
12
|
+
it.
|
|
13
|
+
|
|
14
|
+
That is the right behavior, and it creates one hazard. A layer built against
|
|
15
|
+
`""`, `localhost:4318`, or an endpoint with a trailing newline behaves
|
|
16
|
+
identically to a working one: it builds, it runs, it fails nothing, and it
|
|
17
|
+
delivers nothing forever. Nobody notices until an operator goes looking for a
|
|
18
|
+
trace that was never sent.
|
|
19
|
+
|
|
20
|
+
## Acquisition is the only place to report it
|
|
21
|
+
|
|
22
|
+
So both configuration inputs are decoded while the layer is being built, before
|
|
23
|
+
any exporter exists:
|
|
24
|
+
|
|
25
|
+
- The collector endpoint, by `Endpoint.decode`, failing with
|
|
26
|
+
`Endpoint.InvalidExporterEndpoint`.
|
|
27
|
+
- The service identity and resource attributes, by `Resource.decode`, failing
|
|
28
|
+
with `Resource.InvalidResourceConfiguration`.
|
|
29
|
+
|
|
30
|
+
Both are typed failures in the layer's error channel, which means a composition
|
|
31
|
+
sees them at startup, in the place where the value was written, rather than as
|
|
32
|
+
silence in a dashboard weeks later. `Otlp.layer`, `Otlp.layerFetch`, and
|
|
33
|
+
`NodeOtel.layerOtel` all decode their endpoint; every builder decodes its
|
|
34
|
+
resource.
|
|
35
|
+
|
|
36
|
+
## What an endpoint must be
|
|
37
|
+
|
|
38
|
+
An absolute `http://` or `https://` URL, at most 2,048 characters, carrying no
|
|
39
|
+
username, password, query, fragment, backslashes, spaces, or control characters.
|
|
40
|
+
Base paths such as `https://collector.example/tenant/9` are supported.
|
|
41
|
+
|
|
42
|
+
Queries and fragments are refused because exporters append `/v1/<signal>` to
|
|
43
|
+
the base. Appending it after `?tenant=9` or `#notes` would leave the HTTP request
|
|
44
|
+
path unchanged, silently sending telemetry to the wrong route. Use exporter
|
|
45
|
+
headers for authentication, not credentials embedded in the URL.
|
|
46
|
+
|
|
47
|
+
The last clause is the one that surprises people, and it is the whole reason
|
|
48
|
+
`Endpoint` exists rather than a bare `new URL` check. The WHATWG URL parser is a
|
|
49
|
+
repairer, not a validator: it strips leading and trailing spaces and C0
|
|
50
|
+
controls, and removes tab, newline, and carriage return from anywhere in its
|
|
51
|
+
input. So `new URL("http://collector:4318\n")` succeeds, and reports a
|
|
52
|
+
well-formed URL, while the untrimmed original is what a builder would hand its
|
|
53
|
+
exporter. An endpoint read from a config file with a trailing newline, or
|
|
54
|
+
pasted with a leading space, is the most common shape of this mistake, and it
|
|
55
|
+
produces exactly the silent no-delivery the refusal exists to prevent. No legal
|
|
56
|
+
collector endpoint carries one of those characters, so refusing them costs
|
|
57
|
+
nothing.
|
|
58
|
+
|
|
59
|
+
Repeated trailing separators are normalized away instead of refused:
|
|
60
|
+
`http://host//` and `http://host` both post to `http://host/v1/traces`.
|
|
61
|
+
|
|
62
|
+
## What a resource must be
|
|
63
|
+
|
|
64
|
+
`Resource.Configuration` bounds the identity so an accepted value cannot become
|
|
65
|
+
an unencodable payload later:
|
|
66
|
+
|
|
67
|
+
- `serviceName` and `serviceVersion` are non-empty, well formed, and at most
|
|
68
|
+
1,024 UTF-16 code units.
|
|
69
|
+
- At most 256 attributes, with non-empty, well formed keys of at most 1,024
|
|
70
|
+
code units.
|
|
71
|
+
- Attribute values are finite numbers, booleans, strings of at most 65,536 code
|
|
72
|
+
units, or homogeneous arrays of one of those scalar types with at most 256
|
|
73
|
+
elements (`Resource.maximumAttributeArrayLength`).
|
|
74
|
+
- The complete OTLP JSON resource is at most 128 KiB
|
|
75
|
+
(`Resource.maximumResourceBytes`), including identity, SDK-added
|
|
76
|
+
`telemetry.sdk.name` and `telemetry.sdk.language`, keys, values, array wrappers,
|
|
77
|
+
escaping and UTF-8. SDK fields are reserved even for the default Effect
|
|
78
|
+
exporter. Counting stops as soon as the budget is exceeded;
|
|
79
|
+
the value-free refusal names `attributes`, the measured lower bound and limit.
|
|
80
|
+
Per-field limits alone cannot guarantee this cumulative bound.
|
|
81
|
+
- Resource metadata is explicit. The default OTLP and Node SDK layers isolate
|
|
82
|
+
ambient resource configuration so it cannot enlarge metadata after admission.
|
|
83
|
+
- The 1 MiB transport reserves 8 KiB for the JSON envelope and escaped scope
|
|
84
|
+
name, leaving 888 KiB (`Otlp.reservedBatchBytes`) for a signal batch. This is
|
|
85
|
+
about 909 bytes per record at the upstream 1,000-record log or span limit,
|
|
86
|
+
enough for ordinary batches; application records can still be too large.
|
|
87
|
+
- NUL and unpaired UTF-16 surrogates are refused anywhere. Valid astral Unicode
|
|
88
|
+
is preserved.
|
|
89
|
+
|
|
90
|
+
An empty `serviceName` is refused rather than defaulted, because anonymous
|
|
91
|
+
telemetry in a shared collector is worse than none.
|
|
92
|
+
|
|
93
|
+
## Refusals never carry the rejected value
|
|
94
|
+
|
|
95
|
+
Both errors report a stable `code`, the `path` of the option the bad value
|
|
96
|
+
arrived on, and a message. Neither retains the value itself: `baseUrl` is the
|
|
97
|
+
field most likely to carry a token in a query string or a credential in its
|
|
98
|
+
userinfo, and an error message is the least controlled surface in a system. The
|
|
99
|
+
`path` is what you need to find the value, and you already have the value.
|
|
100
|
+
|
|
101
|
+
The `path` names the caller's own option, so a refusal from `Otlp` says
|
|
102
|
+
`baseUrl` and one from `NodeOtel` says `endpoint`.
|
|
103
|
+
|
|
104
|
+
For what to do when you see one, see
|
|
105
|
+
[Troubleshooting](../troubleshooting.md).
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Export to an OTLP collector"
|
|
3
|
+
description: "Wire Otlp.layerFetch into a running program: the collector endpoint, the service identity, authentication headers, export cadence, the shutdown flush, and what happens when the collector is down."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 1
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
`Otlp` posts logs, metrics, and traces to an OTLP/HTTP collector as JSON. This
|
|
9
|
+
guide covers the options you set once at startup and the behavior you should
|
|
10
|
+
expect from them afterwards.
|
|
11
|
+
|
|
12
|
+
## Provide the layer
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import * as Otlp from "@smthrs/observability/Otlp"
|
|
16
|
+
import * as Effect from "effect/Effect"
|
|
17
|
+
|
|
18
|
+
const telemetry = Otlp.layerFetch({
|
|
19
|
+
baseUrl: "http://localhost:4318",
|
|
20
|
+
serviceName: "deploy-status"
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// `program` is your application effect, unchanged from its non-telemetry form.
|
|
24
|
+
const outcome = program.pipe(Effect.provide(telemetry), Effect.scoped)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The layer is scoped: give it a lifetime with `Effect.scoped`, or build it as
|
|
28
|
+
part of a runtime that owns a scope. Signals post below `baseUrl` at
|
|
29
|
+
`/v1/logs`, `/v1/metrics`, and `/v1/traces`, so a base URL with a path prefix
|
|
30
|
+
is honored: `http://collector:4318/tenant/9` posts to
|
|
31
|
+
`http://collector:4318/tenant/9/v1/traces`.
|
|
32
|
+
|
|
33
|
+
`Otlp.layer` is the same wiring without the `fetch` binding. Use it when the
|
|
34
|
+
host must supply its own HTTP client:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import * as NodeHost from "@smthrs/platform-node/NodeHost"
|
|
38
|
+
import * as Layer from "effect/Layer"
|
|
39
|
+
|
|
40
|
+
const telemetry = Otlp.layer({ baseUrl: "http://localhost:4318" }).pipe(
|
|
41
|
+
Layer.provide(NodeHost.NodeHttpClient.layerUndici)
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Name the service
|
|
46
|
+
|
|
47
|
+
`serviceName` and `serviceVersion` become the `service.name` and
|
|
48
|
+
`service.version` resource attributes on every exported signal, and they are
|
|
49
|
+
how a collector holding several processes tells them apart. Omitted, they fall
|
|
50
|
+
back to `Otlp.defaultServiceName` (`flows`) and `Otlp.defaultServiceVersion`
|
|
51
|
+
(this package's release version), which is a reasonable default for a Smithers
|
|
52
|
+
CLI process and a poor one for your application.
|
|
53
|
+
|
|
54
|
+
Add deployment facts as `attributes`:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const telemetry = Otlp.layerFetch({
|
|
58
|
+
baseUrl: "https://otlp.example.com",
|
|
59
|
+
serviceName: "deploy-status",
|
|
60
|
+
serviceVersion: "2.4.1",
|
|
61
|
+
attributes: {
|
|
62
|
+
"deployment.environment.name": "production",
|
|
63
|
+
"service.instance.id": process.env.HOSTNAME ?? "local"
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Attribute values are strings, finite numbers, booleans, or homogeneous arrays
|
|
69
|
+
of those. The full bounds are in
|
|
70
|
+
[Validation at layer acquisition](../concepts/validated-acquisition.md).
|
|
71
|
+
|
|
72
|
+
## Authenticate
|
|
73
|
+
|
|
74
|
+
`headers` is sent with every export request, which is where a vendor token
|
|
75
|
+
goes:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const telemetry = Otlp.layerFetch({
|
|
79
|
+
baseUrl: "https://otlp.example.com",
|
|
80
|
+
serviceName: "deploy-status",
|
|
81
|
+
headers: { authorization: "Bearer YOUR_TOKEN" }
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Read the token from the environment or your secret store. A credential in the
|
|
86
|
+
URL is refused: an endpoint carrying userinfo fails layer acquisition. A
|
|
87
|
+
credential header (`authorization`, `x-api-key`, `x-honeycomb-team`, and
|
|
88
|
+
similar names) over plaintext `http://` is refused too, unless the collector is
|
|
89
|
+
on `localhost`, `127.0.0.0/8`, or `[::1]`. Non-credential headers such as a
|
|
90
|
+
tenant id work on any endpoint. The check reads header names only, so use
|
|
91
|
+
`https://` for every remote collector.
|
|
92
|
+
|
|
93
|
+
Fetch redirects are refused, including same-origin redirects. Configure the
|
|
94
|
+
collector's final URL. Other HTTP transports must refuse redirects themselves.
|
|
95
|
+
For fetch options such as `credentials: "omit"`, provide
|
|
96
|
+
`FetchHttpClient.RequestInit` around the whole exporting effect:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"
|
|
100
|
+
|
|
101
|
+
const outcome = program.pipe(
|
|
102
|
+
Effect.provide(telemetry),
|
|
103
|
+
Effect.provideService(FetchHttpClient.RequestInit, { credentials: "omit" }),
|
|
104
|
+
Effect.scoped
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Options supplied only to the inner HTTP-client layer are replaced by the
|
|
109
|
+
redirect policy. Ambient options are retained except `redirect`, which is
|
|
110
|
+
always `"manual"`. Redirect responses fail export through the existing failure
|
|
111
|
+
handling below; they never forward credentials or signal bodies.
|
|
112
|
+
|
|
113
|
+
## Control the cadence and the flush
|
|
114
|
+
|
|
115
|
+
`exportInterval` sets how often all three signals batch and post. Omitted, each
|
|
116
|
+
signal keeps Effect's own default. `shutdownTimeout` bounds the final flush
|
|
117
|
+
when the layer's scope closes.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const telemetry = Otlp.layerFetch({
|
|
121
|
+
baseUrl: "http://localhost:4318",
|
|
122
|
+
serviceName: "deploy-status",
|
|
123
|
+
exportInterval: "10 seconds",
|
|
124
|
+
shutdownTimeout: "5 seconds"
|
|
125
|
+
})
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Closing the scope is the deterministic flush. A short program may deliver
|
|
129
|
+
nothing on the interval and everything at shutdown, so read what a collector
|
|
130
|
+
received only after the scope has closed.
|
|
131
|
+
|
|
132
|
+
## Turn export off explicitly
|
|
133
|
+
|
|
134
|
+
`Otlp.layerNoop` provides nothing and exports nothing. It exists so a host that
|
|
135
|
+
has no collector, or a browser build that has not opted in, switches layers
|
|
136
|
+
rather than branching around a missing provide:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const telemetry = collectorUrl === undefined
|
|
140
|
+
? Otlp.layerNoop
|
|
141
|
+
: Otlp.layerFetch({ baseUrl: collectorUrl, serviceName: "deploy-status" })
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## What happens when the collector is down
|
|
145
|
+
|
|
146
|
+
Export failure never fails your program, and it never surfaces as a rejected
|
|
147
|
+
promise. Effect's exporter retries a transient failure three times, honoring a
|
|
148
|
+
`retry-after` response header when one is present, then disables that exporter
|
|
149
|
+
for 60 seconds and logs a `Debug` record saying so. Spans and records produced
|
|
150
|
+
inside that window are dropped, and delivery resumes afterwards.
|
|
151
|
+
|
|
152
|
+
Two consequences worth planning for:
|
|
153
|
+
|
|
154
|
+
- A dead collector is invisible at `Info`. Run at `Debug` while you are
|
|
155
|
+
bringing an exporter up, or watch the collector side.
|
|
156
|
+
- A configuration mistake and a collector outage look the same after
|
|
157
|
+
acquisition. That is why the endpoint is decoded before any exporter exists;
|
|
158
|
+
see [Troubleshooting](../troubleshooting.md) when nothing arrives.
|
|
159
|
+
|
|
160
|
+
## Next steps
|
|
161
|
+
|
|
162
|
+
- [Test telemetry without a collector](./testing.md): assert on what would have
|
|
163
|
+
been posted.
|
|
164
|
+
- [Wire an OpenTelemetry SDK](./wire-an-opentelemetry-sdk.md): when a collector
|
|
165
|
+
endpoint is not enough.
|
|
166
|
+
- [Read the runtime metrics](./read-runtime-metrics.md): what the exported
|
|
167
|
+
series mean.
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Forward logs to the run journal"
|
|
3
|
+
description: "Install JournalLogger.layerJournalForwarding so a run's Effect log records become durable telemetry.log entries, and read them back: capacity, bounds, redaction, observed losses, and admission limits."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 3
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
`JournalLogger` turns a run's Effect log records into durable, structured
|
|
9
|
+
`telemetry.log` entries on that run's journal. It is the second delivery path
|
|
10
|
+
beside [OTLP export](./export-to-a-collector.md), and it is the one an operator
|
|
11
|
+
reads without a collector: the records land next to the run's lifecycle events
|
|
12
|
+
and are read back with the same `Journal.entries` call.
|
|
13
|
+
|
|
14
|
+
## Install the layer
|
|
15
|
+
|
|
16
|
+
The layer names one run, and requires a `Journal.Journal` service from
|
|
17
|
+
[`@smthrs/journal`](/api/journal):
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import * as JournalEvent from "@smthrs/journal/JournalEvent"
|
|
21
|
+
import * as JournalLogger from "@smthrs/observability/JournalLogger"
|
|
22
|
+
import * as Schema from "effect/Schema"
|
|
23
|
+
|
|
24
|
+
const runId = Schema.decodeUnknownSync(JournalEvent.RunId)("deploy-status-1")
|
|
25
|
+
|
|
26
|
+
const journalLogs = JournalLogger.layerJournalForwarding({ runId })
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`runId` is branded, so it comes from decoding a string rather than from a bare
|
|
30
|
+
literal. Decoding it here means a malformed id is reported where it was
|
|
31
|
+
written.
|
|
32
|
+
|
|
33
|
+
Provide the layer over your journal. In a test, the deterministic bundle is
|
|
34
|
+
enough:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import * as TestJournal from "@smthrs/journal/test/TestJournal"
|
|
38
|
+
import * as Layer from "effect/Layer"
|
|
39
|
+
|
|
40
|
+
const layer = Layer.provideMerge(journalLogs, TestJournal.layer())
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
In production, the journal is the one your durable engine already composed;
|
|
44
|
+
provide the forwarder over it the same way.
|
|
45
|
+
|
|
46
|
+
## Read the records back
|
|
47
|
+
|
|
48
|
+
Each record is one journal entry with `eventType` `telemetry.log`, written on
|
|
49
|
+
the journal's lossy channel by source `flows/observability/logger`. The payload
|
|
50
|
+
decodes with the exported schema:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import * as Journal from "@smthrs/journal/Journal"
|
|
54
|
+
import * as Effect from "effect/Effect"
|
|
55
|
+
|
|
56
|
+
const read = Effect.gen(function*() {
|
|
57
|
+
const journal = yield* Journal.Journal
|
|
58
|
+
const page = yield* journal.entries({ runId, limit: 200 })
|
|
59
|
+
return page.entries
|
|
60
|
+
.filter((entry) => entry.eventType === "telemetry.log")
|
|
61
|
+
.map((entry) => Schema.decodeUnknownSync(JournalLogger.TelemetryLog)(entry.payload))
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A decoded record carries the level, the logged message, the log annotations,
|
|
66
|
+
the fiber id, an ISO-8601 timestamp, the active span's `traceId` and `spanId`
|
|
67
|
+
when there was one, and a structural `cause` that preserves the order and the
|
|
68
|
+
kind of every failure reason:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
{
|
|
72
|
+
version: 1,
|
|
73
|
+
level: "Error",
|
|
74
|
+
message: [ "deploy failed" ],
|
|
75
|
+
annotations: { lane: "api", attempt: 2 },
|
|
76
|
+
cause: {
|
|
77
|
+
version: 1,
|
|
78
|
+
reasons: [
|
|
79
|
+
{ _tag: "Fail", error: { code: "timeout" } },
|
|
80
|
+
{ _tag: "Interrupt", fiberId: 7 }
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
fiberId: 12,
|
|
84
|
+
traceId: "0af7651916cd43dd8448eb211c80319c",
|
|
85
|
+
spanId: "b7ad6b7169203331",
|
|
86
|
+
timestamp: "2026-09-03T12:00:00.000Z"
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The durable journal allocates the per-source sequence, so restarting the layer
|
|
91
|
+
or running two of them for one run cannot make two records collide on one
|
|
92
|
+
identity.
|
|
93
|
+
|
|
94
|
+
## Choose the queue depth
|
|
95
|
+
|
|
96
|
+
The logger callback never blocks. It snapshots the record synchronously when the
|
|
97
|
+
bounded queue has room, then offers it to that queue, which a forked worker
|
|
98
|
+
drains into the journal.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const journalLogs = JournalLogger.layerJournalForwarding({
|
|
102
|
+
runId,
|
|
103
|
+
capacity: 4096,
|
|
104
|
+
minimumLogLevel: "Debug",
|
|
105
|
+
mergeWithExisting: true
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- `capacity` defaults to 256 and accepts 1 through
|
|
110
|
+
`JournalLogger.maximumCapacity` (65,536). Raise it for a chatty run on a slow
|
|
111
|
+
journal; a full queue drops the incoming record before snapshotting it, so
|
|
112
|
+
saturation costs no traversal of the logged value.
|
|
113
|
+
- `minimumLogLevel` pins `References.MinimumLogLevel` for the whole
|
|
114
|
+
application. Omitted, the layer leaves that reference alone and Effect's own
|
|
115
|
+
`Info` default applies.
|
|
116
|
+
- `mergeWithExisting` defaults to `false`, which replaces the ambient logger
|
|
117
|
+
set. Pass `true` to keep a console logger alongside the forwarder. See
|
|
118
|
+
[Install a logger](./install-a-logger.md).
|
|
119
|
+
|
|
120
|
+
An invalid run id or capacity fails layer acquisition with
|
|
121
|
+
`InvalidJournalLoggerOptions`, before any worker starts.
|
|
122
|
+
|
|
123
|
+
## What the snapshot does to a value
|
|
124
|
+
|
|
125
|
+
The record is detached, bounded, and redacted before it joins the queue, so
|
|
126
|
+
mutating a logged object after the log call cannot change what is persisted,
|
|
127
|
+
and a hostile value cannot stall the logger.
|
|
128
|
+
|
|
129
|
+
| Ceiling | Value | What happens past it |
|
|
130
|
+
| ------------------------ | ----- | ------------------------------------------------ |
|
|
131
|
+
| `maximumSnapshotBytes` | 1 MiB | Text is cut and marked `[Truncated]` |
|
|
132
|
+
| `maximumSnapshotMembers` | 4,096 | Remaining members become one `[Truncated]` entry |
|
|
133
|
+
| `maximumSnapshotDepth` | 64 | Deeper values become `[Deep]` |
|
|
134
|
+
|
|
135
|
+
Values that cannot be read at all, such as a throwing accessor or a revoked
|
|
136
|
+
proxy, become `[Unrenderable]`. Functions, symbols, and binary data are named
|
|
137
|
+
rather than serialized (`[Function]`, `[Symbol]`, `[Binary]`), and a cycle
|
|
138
|
+
becomes `[Circular]`. Text is bounded in one pass that never cuts a surrogate
|
|
139
|
+
pair, so a truncated string is still well formed.
|
|
140
|
+
|
|
141
|
+
An `Error` is projected to `name`, `message`, `stack`, and `cause` first, then
|
|
142
|
+
to every other own data property it carries, under the same member and byte
|
|
143
|
+
budgets. A `Schema.TaggedError` therefore keeps its `_tag` and its declared
|
|
144
|
+
fields, and a Node system error keeps `code`, `syscall`, and `path`. Accessor
|
|
145
|
+
properties become `[Unrenderable]`, and members past the budget become one
|
|
146
|
+
`[Truncated]` entry.
|
|
147
|
+
|
|
148
|
+
Container-shaped fields keep their shape. Annotations whose snapshot spent the
|
|
149
|
+
whole budget arrive as `{ "[Truncated]": "[Truncated]" }` rather than as a
|
|
150
|
+
scalar the schema would refuse. A projection that still fails `TelemetryLog`
|
|
151
|
+
degrades to a total record with an empty cause, so every durable
|
|
152
|
+
`telemetry.log` row decodes.
|
|
153
|
+
|
|
154
|
+
The journal's own redaction rules run before queue admission, the same scrub
|
|
155
|
+
durable events get, so a credential in a logged object never reaches a
|
|
156
|
+
permanent row.
|
|
157
|
+
|
|
158
|
+
## Watch for lost records
|
|
159
|
+
|
|
160
|
+
Forwarding is lossy on purpose: a telemetry backlog must not become application
|
|
161
|
+
backpressure. Each of these observed losses advances `Metric.droppedLogRecords`
|
|
162
|
+
(`flows_observability_log_dropped`) once:
|
|
163
|
+
|
|
164
|
+
1. **Forwarder queue overflow.** The queue was full when the record arrived.
|
|
165
|
+
Counted only.
|
|
166
|
+
2. **Journal `Dropped` receipt.** The journal refused admission under its
|
|
167
|
+
`drop-newest` policy. Counted only; `Accepted` and `Duplicate` receipts do
|
|
168
|
+
not advance the counter.
|
|
169
|
+
3. **Journal delivery failure.** Admission failed. Counted, and reported as a
|
|
170
|
+
warning with the run id and code `journal_forwarding_failed`.
|
|
171
|
+
4. **A defect from the journal implementation.** Counted and warned with the
|
|
172
|
+
run id and code `journal_forwarding_defect`. The worker keeps draining.
|
|
173
|
+
|
|
174
|
+
Warnings carry fixed diagnostic codes instead of raw errors or causes, so
|
|
175
|
+
failure credentials do not reach ambient warning sinks. The worker is forked
|
|
176
|
+
before the forwarding logger is installed, so a warning cannot enqueue itself.
|
|
177
|
+
|
|
178
|
+
`emitLossy` reports admission, not durable delivery. The counter cannot observe
|
|
179
|
+
later `drop-oldest` evictions of admitted telemetry, asynchronous persistence
|
|
180
|
+
failures, or journal shutdown losses. An `Accepted.evicted` summary does not
|
|
181
|
+
identify the evicted record, so it cannot attribute that loss to telemetry.
|
|
182
|
+
|
|
183
|
+
Interruption stays fatal: closing the layer's scope ends the worker and can
|
|
184
|
+
drop records queued behind an in-flight write without advancing this counter.
|
|
185
|
+
See [Assert on a short-lived run](#assert-on-a-short-lived-run) for the shape a
|
|
186
|
+
test needs because of that.
|
|
187
|
+
|
|
188
|
+
A nonzero `droppedLogRecords` with a healthy journal usually means the capacity
|
|
189
|
+
is too small for the run's log volume. See
|
|
190
|
+
[Read the runtime metrics](./read-runtime-metrics.md) for how to read the
|
|
191
|
+
counter.
|
|
192
|
+
|
|
193
|
+
## Assert on a short-lived run
|
|
194
|
+
|
|
195
|
+
The forwarder exposes no drain barrier. `journal.flush` settles the entries the
|
|
196
|
+
journal has already admitted, so it returns while a record is still waiting in
|
|
197
|
+
the forwarder's queue or inside an admission the worker has not finished.
|
|
198
|
+
Closing the layer's scope then interrupts that worker and discards what is
|
|
199
|
+
left, so flushing before an assertion proves nothing about a record the journal
|
|
200
|
+
never saw.
|
|
201
|
+
|
|
202
|
+
Poll the journal for the records you expect while the layer's scope is still
|
|
203
|
+
open, and let the scope close after the assertion:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
const forwarded = Effect.gen(function*() {
|
|
207
|
+
const journal = yield* Journal.Journal
|
|
208
|
+
yield* Effect.logInfo("first")
|
|
209
|
+
yield* Effect.logInfo("second")
|
|
210
|
+
for (let attempt = 0; attempt < 200; attempt++) {
|
|
211
|
+
const page = yield* journal.entries({ runId, limit: 100 })
|
|
212
|
+
const records = page.entries.filter((entry) => entry.eventType === "telemetry.log")
|
|
213
|
+
if (records.length >= 2) return records
|
|
214
|
+
yield* Effect.sleep("5 millis")
|
|
215
|
+
}
|
|
216
|
+
return []
|
|
217
|
+
}).pipe(
|
|
218
|
+
Effect.provide(Layer.provideMerge(JournalLogger.layerJournalForwarding({ runId }), TestJournal.layer())),
|
|
219
|
+
Effect.scoped
|
|
220
|
+
)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
A bounded poll fails the assertion when a record never arrives, where a fixed
|
|
224
|
+
sleep only makes that failure intermittent. The same pattern is in
|
|
225
|
+
[Test telemetry without a collector](./testing.md), and the loss it guards
|
|
226
|
+
against is cataloged in [Troubleshooting](../troubleshooting.md).
|