@smthrs/observability 0.35.0 → 1.0.0-rc.12
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 +87 -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 +415 -0
- package/dist/cjs/JournalLogger.js.map +7 -0
- package/dist/cjs/Logger.d.ts +56 -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 +63 -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 +203 -0
- package/dist/cjs/Otlp.d.ts.map +1 -0
- package/dist/cjs/Otlp.js +251 -0
- package/dist/cjs/Otlp.js.map +7 -0
- package/dist/cjs/Resource.d.ts +163 -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 +520 -0
- package/dist/esm/JournalLogger.js.map +1 -0
- package/dist/esm/Logger.d.ts +56 -0
- package/dist/esm/Logger.d.ts.map +1 -0
- package/dist/esm/Logger.js +61 -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 +63 -0
- package/dist/esm/Otel.d.ts.map +1 -0
- package/dist/esm/Otel.js +52 -0
- package/dist/esm/Otel.js.map +1 -0
- package/dist/esm/Otlp.d.ts +203 -0
- package/dist/esm/Otlp.d.ts.map +1 -0
- package/dist/esm/Otlp.js +384 -0
- package/dist/esm/Otlp.js.map +1 -0
- package/dist/esm/Resource.d.ts +163 -0
- package/dist/esm/Resource.d.ts.map +1 -0
- package/dist/esm/Resource.js +238 -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 +164 -0
- package/docs/api.md +780 -0
- package/docs/concepts/instrumentation-and-export.md +98 -0
- package/docs/concepts/layer-map.md +73 -0
- package/docs/concepts/validated-acquisition.md +106 -0
- package/docs/guides/export-to-a-collector.md +190 -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 +226 -0
- package/docs/installation.md +102 -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 +635 -0
- package/src/Logger.ts +107 -0
- package/src/Metric.ts +88 -0
- package/src/NodeOtel.ts +90 -0
- package/src/Otel.ts +118 -0
- package/src/Otlp.ts +514 -0
- package/src/Resource.ts +325 -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 -843
- 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 -1392
- package/src/index.js +0 -137
- package/src/kindPhase.js +0 -17
- package/src/logging.js +0 -264
- 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 -1222
- 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,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).
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Install a logger"
|
|
3
|
+
description: "Pick a Logger layer for a development shell, a production process, or a silent test, and understand the two rules every one of them follows: the ambient set is replaced, and the minimum level is left alone."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 4
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
`Logger` ships four layers over Effect's own logger machinery. They differ in
|
|
9
|
+
the sink; they agree on the two rules that decide what installing one does to
|
|
10
|
+
the rest of the application.
|
|
11
|
+
|
|
12
|
+
## Pick a layer
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import * as Logger from "@smthrs/observability/Logger"
|
|
16
|
+
|
|
17
|
+
// A development shell: colored, human-readable lines.
|
|
18
|
+
const development = Logger.layerPrettyDev()
|
|
19
|
+
|
|
20
|
+
// A production process: one structured JSON object per line.
|
|
21
|
+
const production = Logger.layerStructuredJson()
|
|
22
|
+
|
|
23
|
+
// A test that should print nothing.
|
|
24
|
+
const silent = Logger.layerNoop()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`Logger.layerPrettyDev` chooses colors and mode from the terminal it finds, so
|
|
28
|
+
it degrades to plain text when the output is not a TTY.
|
|
29
|
+
`Logger.layerStructuredJson` is the one to ship: a log shipper can parse it, and
|
|
30
|
+
it is what a collector's log pipeline expects.
|
|
31
|
+
|
|
32
|
+
To install a logger you wrote, or one from another package, use `Logger.layer`:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import * as EffectLogger from "effect/Logger"
|
|
36
|
+
|
|
37
|
+
const custom = Logger.layer(
|
|
38
|
+
EffectLogger.make((options) => {
|
|
39
|
+
/* your sink */
|
|
40
|
+
})
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Rule one: the ambient set is replaced
|
|
45
|
+
|
|
46
|
+
`mergeWithExisting` defaults to `false`, matching Effect's own `Logger.layer`,
|
|
47
|
+
so installing one of these layers replaces the ambient logger set rather than
|
|
48
|
+
adding to it. That is what you want for a process that has exactly one sink,
|
|
49
|
+
and it is the usual explanation for logs disappearing after a layer was added.
|
|
50
|
+
|
|
51
|
+
Pass `true` to keep the loggers already installed:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const both = Logger.layerStructuredJson({ mergeWithExisting: true })
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The same option is on
|
|
58
|
+
[`JournalLogger.layerJournalForwarding`](./forward-logs-to-the-journal.md), and
|
|
59
|
+
merging is how a control-plane run gets console output and durable
|
|
60
|
+
`telemetry.log` rows from one log call.
|
|
61
|
+
|
|
62
|
+
## Rule two: the minimum level is left alone
|
|
63
|
+
|
|
64
|
+
A layer that adds a sink has no business overriding the level an application
|
|
65
|
+
chose elsewhere, so `References.MinimumLogLevel` is provided only when you name
|
|
66
|
+
`minimumLogLevel`. Omitted, whatever the application set stands, and Effect's
|
|
67
|
+
own `Info` default applies when nothing set it.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// Leaves the application's chosen level in place.
|
|
71
|
+
const inherits = Logger.layerStructuredJson()
|
|
72
|
+
|
|
73
|
+
// Pins Warn for the whole application.
|
|
74
|
+
const pinned = Logger.layerStructuredJson({ minimumLogLevel: "Warn" })
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`Logger.layerNoop` is the deliberate exception. Silence is its purpose, so it
|
|
78
|
+
pins `None` unless you name another level. Passing one is how a test silences
|
|
79
|
+
output while keeping a level assertion meaningful:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const quietButFiltered = Logger.layerNoop({ minimumLogLevel: "Debug" })
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Merge when you install more than one sink
|
|
86
|
+
|
|
87
|
+
Installing a logger decides what is rendered locally. It exports nothing on its
|
|
88
|
+
own: log records reach a collector because [`Otlp`](./export-to-a-collector.md)
|
|
89
|
+
adds its own log exporter to the same ambient set, and they reach a run's
|
|
90
|
+
history because
|
|
91
|
+
[`JournalLogger`](./forward-logs-to-the-journal.md) adds a forwarder to it.
|
|
92
|
+
|
|
93
|
+
That shared set is why rule one matters here. A `Logger` layer with the default
|
|
94
|
+
`mergeWithExisting: false` discards every logger the set already held, and
|
|
95
|
+
whether that includes the OTLP exporter depends on which layer the composition
|
|
96
|
+
builds last. Pass `mergeWithExisting: true` on the `Logger` layer whenever
|
|
97
|
+
another sink is installed, and the ordering question does not arise:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import * as Effect from "effect/Effect"
|
|
101
|
+
|
|
102
|
+
const outcome = program.pipe(
|
|
103
|
+
Effect.provide(Logger.layerStructuredJson({ mergeWithExisting: true })),
|
|
104
|
+
Effect.provide(telemetry),
|
|
105
|
+
Effect.scoped
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
One `Effect.logInfo` call then renders locally, posts to the collector's
|
|
110
|
+
`/v1/logs` path, and, with the journal forwarder installed, lands on the run's
|
|
111
|
+
durable history.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Read the runtime metrics"
|
|
3
|
+
description: "The four runtime metric handles this package declares, the exact series names a dashboard binds to, which package advances each one, and how to read a counter in process without an exporter."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 5
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
`Metric` declares the runtime signals that cross package boundaries: the
|
|
9
|
+
handles a producer updates and a dashboard reads. Every other metric in
|
|
10
|
+
Smithers belongs to the package that owns the behavior it measures.
|
|
11
|
+
|
|
12
|
+
## The six handles
|
|
13
|
+
|
|
14
|
+
| Handle | Series name | Kind | Advances when |
|
|
15
|
+
| ---------------------------- | ------------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
16
|
+
| `runThroughput` | `flows_run_throughput` | counter | A run's terminal transition commits in [`@smthrs/run-store`](/api/run-store). |
|
|
17
|
+
| `activeSeats` | `flows_seat_active` | gauge | Held for the lifetime of a production `Agent.run` stream in [`@smthrs/agent`](/api/agent), and released on success, failure, or interruption. |
|
|
18
|
+
| `quotaParks` | `flows_quota_park` | counter | A sealed quota decision is first executed, and not when that decision is replayed after a wake or a process restart. |
|
|
19
|
+
| `structuredOutputRejections` | `flows_agent_structured_output_rejections` | counter | An agent answer is refused by its declared output schema, in the same session or a fresh one. |
|
|
20
|
+
| `structuredOutputExhausted` | `flows_agent_structured_output_exhausted` | counter | An agent step fails after spending every output correction and its repair. |
|
|
21
|
+
| `droppedLogRecords` | `flows_observability_log_dropped` | counter | A log record is lost before durable delivery: once per queue overflow, once per journal delivery failure, and once per defect the forwarding worker recovers from. |
|
|
22
|
+
|
|
23
|
+
`Metric.registry` is the same six as one object, so a host can enumerate them.
|
|
24
|
+
The series names are the dashboard contract; treat them as public API.
|
|
25
|
+
|
|
26
|
+
Step-cache lookup and write counters belong to
|
|
27
|
+
[`@smthrs/step-cache`](/api/step-cache), and the store packages keep their own
|
|
28
|
+
handles. This package does not duplicate any of them.
|
|
29
|
+
|
|
30
|
+
## Read a counter in process
|
|
31
|
+
|
|
32
|
+
A metric handle is readable without an exporter, which makes it a fine
|
|
33
|
+
assertion target in a test and a fine health check in a host:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import * as ObservabilityMetric from "@smthrs/observability/Metric"
|
|
37
|
+
import * as Effect from "effect/Effect"
|
|
38
|
+
import * as Metric from "effect/Metric"
|
|
39
|
+
|
|
40
|
+
const dropped = Effect.gen(function*() {
|
|
41
|
+
const value = yield* Metric.value(ObservabilityMetric.droppedLogRecords)
|
|
42
|
+
return value.count
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A counter's value carries `count`; a gauge's carries `value`.
|
|
47
|
+
|
|
48
|
+
## Read a dimensioned counter through its attribute view
|
|
49
|
+
|
|
50
|
+
Some packages update a counter through a tagged view rather than the bare
|
|
51
|
+
handle. The dispatch counter in [`@smthrs/engine-store`](/api/engine-store) is
|
|
52
|
+
the common example: it updates only the series tagged with the outcome, so
|
|
53
|
+
reading the bare handle reads the attribute-less series and always sees zero.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { EngineStoreMetrics } from "@smthrs/engine-store"
|
|
57
|
+
|
|
58
|
+
const dispatches = Effect.gen(function*() {
|
|
59
|
+
const value = yield* Metric.value(EngineStoreMetrics.dispatch.Success)
|
|
60
|
+
return value.count
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Read the exported view, not the parent handle, whenever the owning package
|
|
65
|
+
exports one.
|
|
66
|
+
|
|
67
|
+
## Isolate the registry in a test
|
|
68
|
+
|
|
69
|
+
Effect keeps metric state in a `MetricRegistry` reference with a
|
|
70
|
+
process-default value, so counters accumulate across tests in one process.
|
|
71
|
+
Provide a fresh map to isolate one run:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
Effect.provideService(Metric.MetricRegistry, new Map())
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Where the series go
|
|
78
|
+
|
|
79
|
+
Every registered series exports with the rest of the telemetry: with
|
|
80
|
+
[`Otlp` provided](./export-to-a-collector.md), they post to the collector's
|
|
81
|
+
`/v1/metrics` path under the `service.name` you configured. With no exporter
|
|
82
|
+
installed, they are still collected and still readable in process, and nothing
|
|
83
|
+
leaves the machine.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Test telemetry without a collector"
|
|
3
|
+
description: "Assert on what would have been exported: a recording fetch for OTLP payloads, TestJournal for forwarded log records, a fresh metric registry, and the noop layers that stand in for absent telemetry."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 6
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Telemetry is testable because every outbound edge is a service. Replace the
|
|
9
|
+
edge, keep the wiring, and the assertions are about the payloads a collector
|
|
10
|
+
would have received.
|
|
11
|
+
|
|
12
|
+
## Record what the exporter posts
|
|
13
|
+
|
|
14
|
+
`Otlp.layerFetch` delivers over the `FetchHttpClient.Fetch` service, so
|
|
15
|
+
providing your own function captures every export request and touches no
|
|
16
|
+
network:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import * as Otlp from "@smthrs/observability/Otlp"
|
|
20
|
+
import * as Effect from "effect/Effect"
|
|
21
|
+
import * as FetchHttpClient from "effect/http/FetchHttpClient"
|
|
22
|
+
import * as Metric from "effect/Metric"
|
|
23
|
+
|
|
24
|
+
const recordingFetch = () => {
|
|
25
|
+
const requests: Array<{ url: string; body: string }> = []
|
|
26
|
+
const fetch: typeof globalThis.fetch = (input, init) => {
|
|
27
|
+
const body = typeof init?.body === "string"
|
|
28
|
+
? init.body
|
|
29
|
+
: new TextDecoder().decode(init?.body as Uint8Array)
|
|
30
|
+
requests.push({ url: String(input), body })
|
|
31
|
+
return Promise.resolve(new Response("{}", { status: 200 }))
|
|
32
|
+
}
|
|
33
|
+
return { requests, fetch }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const collector = recordingFetch()
|
|
37
|
+
|
|
38
|
+
await Effect.runPromise(
|
|
39
|
+
program.pipe(
|
|
40
|
+
Effect.provide(Otlp.layerFetch({ baseUrl: "http://collector.invalid:4318" })),
|
|
41
|
+
Effect.provideService(FetchHttpClient.Fetch, collector.fetch),
|
|
42
|
+
Effect.provideService(Metric.MetricRegistry, new Map()),
|
|
43
|
+
Effect.orDie,
|
|
44
|
+
Effect.scoped
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Assert after the scope closes, because the shutdown flush is what delivers a
|
|
50
|
+
short program's signals. Filter `collector.requests` by URL suffix to separate
|
|
51
|
+
the three signals, and parse a body to read span or series names. An
|
|
52
|
+
unresolvable host such as `collector.invalid` is a deliberate choice: if the
|
|
53
|
+
service is ever missed, the test fails rather than reaching the network.
|
|
54
|
+
|
|
55
|
+
The fresh `Metric.MetricRegistry` map keeps one test's counters out of the
|
|
56
|
+
next test's export.
|
|
57
|
+
|
|
58
|
+
## Test the journal forwarder
|
|
59
|
+
|
|
60
|
+
`TestJournal.layer()` from [`@smthrs/journal`](/api/journal) provides the
|
|
61
|
+
production SQLite journal over an in-memory database, so a forwarding test
|
|
62
|
+
exercises the real write path:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import * as Journal from "@smthrs/journal/Journal"
|
|
66
|
+
import * as TestJournal from "@smthrs/journal/test/TestJournal"
|
|
67
|
+
import * as JournalLogger from "@smthrs/observability/JournalLogger"
|
|
68
|
+
import * as Effect from "effect/Effect"
|
|
69
|
+
import * as Layer from "effect/Layer"
|
|
70
|
+
|
|
71
|
+
// `runId` is a decoded JournalEvent.RunId, as in the forwarding guide.
|
|
72
|
+
const entries = Effect.gen(function*() {
|
|
73
|
+
const journal = yield* Journal.Journal
|
|
74
|
+
yield* Effect.logInfo("hello")
|
|
75
|
+
const page = yield* journal.entries({ runId, limit: 100 })
|
|
76
|
+
return page.entries
|
|
77
|
+
}).pipe(
|
|
78
|
+
Effect.provide(Layer.provideMerge(JournalLogger.layerJournalForwarding({ runId }), TestJournal.layer())),
|
|
79
|
+
Effect.scoped
|
|
80
|
+
)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Forwarding is asynchronous, so poll until the expected number of entries has
|
|
84
|
+
landed rather than sleeping a fixed interval. To assert on a loss instead,
|
|
85
|
+
read `Metric.value(ObservabilityMetric.droppedLogRecords)`.
|
|
86
|
+
|
|
87
|
+
## Control time for the retry window
|
|
88
|
+
|
|
89
|
+
Export retries and the 60-second disable window run on Effect's clock, so
|
|
90
|
+
`TestClock` makes them deterministic:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { TestClock } from "effect/testing"
|
|
94
|
+
|
|
95
|
+
const exported = Effect.gen(function*() {
|
|
96
|
+
yield* Effect.void.pipe(Effect.withSpan("exported"))
|
|
97
|
+
// One initial request plus three transient retries completes within four seconds.
|
|
98
|
+
yield* TestClock.adjust("5 seconds")
|
|
99
|
+
})
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Provide `TestClock.layer()` alongside the exporter, then advance the clock and
|
|
103
|
+
count the recorded requests.
|
|
104
|
+
|
|
105
|
+
## Stand telemetry down explicitly
|
|
106
|
+
|
|
107
|
+
A test that is not about telemetry should say so rather than leave the layer
|
|
108
|
+
out and let a type error decide:
|
|
109
|
+
|
|
110
|
+
| Layer | What it does |
|
|
111
|
+
| -------------------- | --------------------------------------------------------------- |
|
|
112
|
+
| `Otlp.layerNoop` | Provides nothing and exports nothing. |
|
|
113
|
+
| `Otel.layerNoop` | The empty provider-neutral OTEL slot. |
|
|
114
|
+
| `Logger.layerNoop()` | Removes the logger set and pins `None` unless you name a level. |
|
|
115
|
+
|
|
116
|
+
`Journal.layerNoop` from [`@smthrs/journal`](/api/journal) is the matching stub
|
|
117
|
+
for the forwarder when the assertion is about queue behavior rather than about
|
|
118
|
+
persistence.
|
|
119
|
+
|
|
120
|
+
## Assert on refusals at acquisition
|
|
121
|
+
|
|
122
|
+
Both configuration refusals happen while the layer is built, so a test builds
|
|
123
|
+
the layer and inspects the exit rather than running a program:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import * as Layer from "effect/Layer"
|
|
127
|
+
|
|
128
|
+
const exit = await Effect.runPromiseExit(
|
|
129
|
+
Effect.scoped(Layer.build(Otlp.layerFetch({ baseUrl: "collector:4318" })))
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The failure is an `Endpoint.InvalidExporterEndpoint` whose `path` is `baseUrl`.
|
|
134
|
+
See [Troubleshooting](../troubleshooting.md) for the catalog.
|