@smthrs/observability 0.35.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.
Files changed (363) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/LICENSE +1 -1
  3. package/README.md +84 -0
  4. package/dist/cjs/BrowserOtel.d.ts +37 -0
  5. package/dist/cjs/BrowserOtel.d.ts.map +1 -0
  6. package/dist/cjs/BrowserOtel.js +47 -0
  7. package/dist/cjs/BrowserOtel.js.map +7 -0
  8. package/dist/cjs/Endpoint.d.ts +70 -0
  9. package/dist/cjs/Endpoint.d.ts.map +1 -0
  10. package/dist/cjs/Endpoint.js +85 -0
  11. package/dist/cjs/Endpoint.js.map +7 -0
  12. package/dist/cjs/JournalLogger.d.ts +232 -0
  13. package/dist/cjs/JournalLogger.d.ts.map +1 -0
  14. package/dist/cjs/JournalLogger.js +388 -0
  15. package/dist/cjs/JournalLogger.js.map +7 -0
  16. package/dist/cjs/Logger.d.ts +55 -0
  17. package/dist/cjs/Logger.d.ts.map +1 -0
  18. package/dist/cjs/Logger.js +72 -0
  19. package/dist/cjs/Logger.js.map +7 -0
  20. package/dist/cjs/Metric.d.ts +67 -0
  21. package/dist/cjs/Metric.d.ts.map +1 -0
  22. package/dist/cjs/Metric.js +68 -0
  23. package/dist/cjs/Metric.js.map +7 -0
  24. package/dist/cjs/NodeOtel.d.ts +42 -0
  25. package/dist/cjs/NodeOtel.d.ts.map +1 -0
  26. package/dist/cjs/NodeOtel.js +78 -0
  27. package/dist/cjs/NodeOtel.js.map +7 -0
  28. package/dist/cjs/Otel.d.ts +62 -0
  29. package/dist/cjs/Otel.d.ts.map +1 -0
  30. package/dist/cjs/Otel.js +75 -0
  31. package/dist/cjs/Otel.js.map +7 -0
  32. package/dist/cjs/Otlp.d.ts +200 -0
  33. package/dist/cjs/Otlp.d.ts.map +1 -0
  34. package/dist/cjs/Otlp.js +235 -0
  35. package/dist/cjs/Otlp.js.map +7 -0
  36. package/dist/cjs/Resource.d.ts +162 -0
  37. package/dist/cjs/Resource.d.ts.map +1 -0
  38. package/dist/cjs/Resource.js +186 -0
  39. package/dist/cjs/Resource.js.map +7 -0
  40. package/dist/cjs/index.d.ts +61 -0
  41. package/dist/cjs/index.d.ts.map +1 -0
  42. package/dist/cjs/index.js +47 -0
  43. package/dist/cjs/index.js.map +7 -0
  44. package/dist/cjs/internal/schemaIssuePath.d.ts +31 -0
  45. package/dist/cjs/internal/schemaIssuePath.d.ts.map +1 -0
  46. package/dist/cjs/internal/schemaIssuePath.js +49 -0
  47. package/dist/cjs/internal/schemaIssuePath.js.map +7 -0
  48. package/dist/cjs/package.json +1 -0
  49. package/dist/esm/BrowserOtel.d.ts +37 -0
  50. package/dist/esm/BrowserOtel.d.ts.map +1 -0
  51. package/dist/esm/BrowserOtel.js +24 -0
  52. package/dist/esm/BrowserOtel.js.map +1 -0
  53. package/dist/esm/Endpoint.d.ts +70 -0
  54. package/dist/esm/Endpoint.d.ts.map +1 -0
  55. package/dist/esm/Endpoint.js +110 -0
  56. package/dist/esm/Endpoint.js.map +1 -0
  57. package/dist/esm/JournalLogger.d.ts +232 -0
  58. package/dist/esm/JournalLogger.d.ts.map +1 -0
  59. package/dist/esm/JournalLogger.js +482 -0
  60. package/dist/esm/JournalLogger.js.map +1 -0
  61. package/dist/esm/Logger.d.ts +55 -0
  62. package/dist/esm/Logger.d.ts.map +1 -0
  63. package/dist/esm/Logger.js +60 -0
  64. package/dist/esm/Logger.js.map +1 -0
  65. package/dist/esm/Metric.d.ts +67 -0
  66. package/dist/esm/Metric.d.ts.map +1 -0
  67. package/dist/esm/Metric.js +80 -0
  68. package/dist/esm/Metric.js.map +1 -0
  69. package/dist/esm/NodeOtel.d.ts +42 -0
  70. package/dist/esm/NodeOtel.d.ts.map +1 -0
  71. package/dist/esm/NodeOtel.js +56 -0
  72. package/dist/esm/NodeOtel.js.map +1 -0
  73. package/dist/esm/Otel.d.ts +62 -0
  74. package/dist/esm/Otel.d.ts.map +1 -0
  75. package/dist/esm/Otel.js +51 -0
  76. package/dist/esm/Otel.js.map +1 -0
  77. package/dist/esm/Otlp.d.ts +200 -0
  78. package/dist/esm/Otlp.d.ts.map +1 -0
  79. package/dist/esm/Otlp.js +350 -0
  80. package/dist/esm/Otlp.js.map +1 -0
  81. package/dist/esm/Resource.d.ts +162 -0
  82. package/dist/esm/Resource.d.ts.map +1 -0
  83. package/dist/esm/Resource.js +237 -0
  84. package/dist/esm/Resource.js.map +1 -0
  85. package/dist/esm/index.d.ts +61 -0
  86. package/dist/esm/index.d.ts.map +1 -0
  87. package/dist/esm/index.js +61 -0
  88. package/dist/esm/index.js.map +1 -0
  89. package/dist/esm/internal/schemaIssuePath.d.ts +31 -0
  90. package/dist/esm/internal/schemaIssuePath.d.ts.map +1 -0
  91. package/dist/esm/internal/schemaIssuePath.js +59 -0
  92. package/dist/esm/internal/schemaIssuePath.js.map +1 -0
  93. package/docs/README.md +162 -0
  94. package/docs/api.md +768 -0
  95. package/docs/concepts/instrumentation-and-export.md +68 -0
  96. package/docs/concepts/layer-map.md +72 -0
  97. package/docs/concepts/validated-acquisition.md +105 -0
  98. package/docs/guides/export-to-a-collector.md +167 -0
  99. package/docs/guides/forward-logs-to-the-journal.md +226 -0
  100. package/docs/guides/install-a-logger.md +111 -0
  101. package/docs/guides/read-runtime-metrics.md +83 -0
  102. package/docs/guides/testing.md +134 -0
  103. package/docs/guides/wire-an-opentelemetry-sdk.md +220 -0
  104. package/docs/installation.md +98 -0
  105. package/docs/quickstart.md +145 -0
  106. package/docs/troubleshooting.md +168 -0
  107. package/package.json +221 -32
  108. package/src/BrowserOtel.ts +51 -0
  109. package/src/Endpoint.ts +131 -0
  110. package/src/JournalLogger.ts +596 -0
  111. package/src/Logger.ts +106 -0
  112. package/src/Metric.ts +88 -0
  113. package/src/NodeOtel.ts +90 -0
  114. package/src/Otel.ts +117 -0
  115. package/src/Otlp.ts +477 -0
  116. package/src/Resource.ts +324 -0
  117. package/src/index.ts +67 -0
  118. package/src/internal/schemaIssuePath.ts +65 -0
  119. package/docker-compose.otel.yml +0 -104
  120. package/grafana/dashboards/smithers-dashboard.json +0 -453
  121. package/grafana/provisioning/dashboards/dashboards.yml +0 -12
  122. package/grafana/provisioning/datasources/datasources.yml +0 -49
  123. package/loki/loki-config.yaml +0 -35
  124. package/otel-collector-config.yml +0 -38
  125. package/prometheus/prometheus.yml +0 -8
  126. package/src/MetricName.ts +0 -1
  127. package/src/MetricsServiceLive.js +0 -9
  128. package/src/README.md +0 -28
  129. package/src/ResolvedSmithersObservabilityOptions.ts +0 -12
  130. package/src/SmithersEvent.ts +0 -843
  131. package/src/SmithersLogFormat.ts +0 -1
  132. package/src/SmithersMetricDefinition.ts +0 -15
  133. package/src/SmithersMetricType.ts +0 -1
  134. package/src/SmithersMetricUnit.ts +0 -1
  135. package/src/SmithersObservability.js +0 -15
  136. package/src/SmithersObservabilityOptions.ts +0 -12
  137. package/src/SmithersObservabilityService.ts +0 -13
  138. package/src/_coreCorrelation/CorrelationContext.ts +0 -10
  139. package/src/_coreCorrelation/CorrelationContextLive.js +0 -11
  140. package/src/_coreCorrelation/CorrelationContextService.js +0 -15
  141. package/src/_coreCorrelation/CorrelationContextServiceShape.ts +0 -12
  142. package/src/_coreCorrelation/CorrelationPatch.ts +0 -3
  143. package/src/_coreCorrelation/README.md +0 -20
  144. package/src/_coreCorrelation/_correlationStorage.js +0 -4
  145. package/src/_coreCorrelation/correlationContextFiberRef.js +0 -5
  146. package/src/_coreCorrelation/correlationContextToLogAnnotations.js +0 -18
  147. package/src/_coreCorrelation/getCurrentCorrelationContext.js +0 -9
  148. package/src/_coreCorrelation/getCurrentCorrelationContextEffect.js +0 -11
  149. package/src/_coreCorrelation/index.js +0 -14
  150. package/src/_coreCorrelation/mergeCorrelationContext.js +0 -51
  151. package/src/_coreCorrelation/runWithCorrelationContext.js +0 -14
  152. package/src/_coreCorrelation/updateCurrentCorrelationContext.js +0 -17
  153. package/src/_coreCorrelation/withCorrelationContext.js +0 -38
  154. package/src/_coreCorrelation/withCurrentCorrelationContext.js +0 -11
  155. package/src/_coreMetrics.js +0 -14
  156. package/src/_coreMetricsShape.ts +0 -44
  157. package/src/_corePrometheus.js +0 -91
  158. package/src/_corePrometheusShape.ts +0 -11
  159. package/src/_coreTracing.js +0 -132
  160. package/src/_coreTracingShape.ts +0 -17
  161. package/src/_otelLogBuilders.js +0 -102
  162. package/src/_sessionFileResolvers.js +0 -164
  163. package/src/_smithersSpanAttributeAliases.js +0 -19
  164. package/src/_smithersTraceSpanStorage.js +0 -3
  165. package/src/_traceEventNormalizers.js +0 -499
  166. package/src/_traceRedaction.js +0 -162
  167. package/src/agentSessionEventToOtelLogRecord.js +0 -55
  168. package/src/agentTrace.ts +0 -144
  169. package/src/agentTraceCapabilities.js +0 -151
  170. package/src/annotateSmithersTrace.js +0 -11
  171. package/src/canonicalTraceEventToOtelLogRecord.js +0 -55
  172. package/src/correlation.js +0 -56
  173. package/src/createSmithersObservabilityLayer.js +0 -59
  174. package/src/createSmithersOtelLayer.js +0 -23
  175. package/src/createSmithersRuntimeLayer.js +0 -2
  176. package/src/detectAgentFamily.js +0 -26
  177. package/src/detectCaptureMode.js +0 -29
  178. package/src/emitOtelLogRecord.js +0 -19
  179. package/src/getCurrentSmithersTraceAnnotations.js +0 -14
  180. package/src/getCurrentSmithersTraceSpan.js +0 -7
  181. package/src/index.d.ts +0 -1392
  182. package/src/index.js +0 -137
  183. package/src/kindPhase.js +0 -17
  184. package/src/logging.js +0 -264
  185. package/src/makeSmithersSpanAttributes.js +0 -20
  186. package/src/metrics/README.md +0 -22
  187. package/src/metrics/SmithersMetricDefinition.ts +0 -17
  188. package/src/metrics/SmithersMetricType.ts +0 -1
  189. package/src/metrics/SmithersMetricUnit.ts +0 -1
  190. package/src/metrics/_asyncExternalWaitCounts.js +0 -4
  191. package/src/metrics/_buckets.js +0 -37
  192. package/src/metrics/_incrementGauge.js +0 -11
  193. package/src/metrics/_processStartMs.js +0 -1
  194. package/src/metrics/activeNodes.js +0 -2
  195. package/src/metrics/activeRuns.js +0 -2
  196. package/src/metrics/agentActionsTotal.js +0 -2
  197. package/src/metrics/agentDurationMs.js +0 -3
  198. package/src/metrics/agentErrorsTotal.js +0 -2
  199. package/src/metrics/agentEventsTotal.js +0 -2
  200. package/src/metrics/agentInvocationsTotal.js +0 -2
  201. package/src/metrics/agentRetriesTotal.js +0 -2
  202. package/src/metrics/agentSessionsTotal.js +0 -2
  203. package/src/metrics/agentTokensTotal.js +0 -2
  204. package/src/metrics/alertDeliveriesAttempted.js +0 -2
  205. package/src/metrics/alertDeliveriesSuppressed.js +0 -2
  206. package/src/metrics/alertsAcknowledgedTotal.js +0 -2
  207. package/src/metrics/alertsActive.js +0 -2
  208. package/src/metrics/alertsEscalatedTotal.js +0 -2
  209. package/src/metrics/alertsFiredTotal.js +0 -2
  210. package/src/metrics/alertsReopenedTotal.js +0 -2
  211. package/src/metrics/alertsResolvedTotal.js +0 -2
  212. package/src/metrics/alertsSilencedTotal.js +0 -2
  213. package/src/metrics/approvalPending.js +0 -2
  214. package/src/metrics/approvalWaitDuration.js +0 -5
  215. package/src/metrics/approvalsDenied.js +0 -2
  216. package/src/metrics/approvalsGranted.js +0 -2
  217. package/src/metrics/approvalsRequested.js +0 -2
  218. package/src/metrics/attemptDuration.js +0 -3
  219. package/src/metrics/attentionBacklog.js +0 -2
  220. package/src/metrics/cacheHits.js +0 -2
  221. package/src/metrics/cacheMisses.js +0 -2
  222. package/src/metrics/dbQueryDuration.js +0 -3
  223. package/src/metrics/dbRetries.js +0 -2
  224. package/src/metrics/dbTransactionDuration.js +0 -3
  225. package/src/metrics/dbTransactionRetries.js +0 -2
  226. package/src/metrics/dbTransactionRollbacks.js +0 -2
  227. package/src/metrics/devtoolsActiveSubscribers.js +0 -2
  228. package/src/metrics/devtoolsBackpressureDisconnectTotal.js +0 -2
  229. package/src/metrics/devtoolsDeltaBuildMs.js +0 -3
  230. package/src/metrics/devtoolsEventBytes.js +0 -3
  231. package/src/metrics/devtoolsEventTotal.js +0 -2
  232. package/src/metrics/devtoolsSnapshotBuildMs.js +0 -5
  233. package/src/metrics/devtoolsSubscribeTotal.js +0 -2
  234. package/src/metrics/errorsTotal.js +0 -2
  235. package/src/metrics/eventsEmittedTotal.js +0 -2
  236. package/src/metrics/externalWaitAsyncPending.js +0 -2
  237. package/src/metrics/gatewayApprovalDecisionsTotal.js +0 -2
  238. package/src/metrics/gatewayAuthEventsTotal.js +0 -2
  239. package/src/metrics/gatewayConnectionsActive.js +0 -2
  240. package/src/metrics/gatewayConnectionsClosedTotal.js +0 -2
  241. package/src/metrics/gatewayConnectionsTotal.js +0 -2
  242. package/src/metrics/gatewayCronTriggersTotal.js +0 -2
  243. package/src/metrics/gatewayErrorsTotal.js +0 -2
  244. package/src/metrics/gatewayHeartbeatTicksTotal.js +0 -2
  245. package/src/metrics/gatewayMessagesReceivedTotal.js +0 -2
  246. package/src/metrics/gatewayMessagesSentTotal.js +0 -2
  247. package/src/metrics/gatewayRpcCallsTotal.js +0 -2
  248. package/src/metrics/gatewayRpcDuration.js +0 -3
  249. package/src/metrics/gatewayRunEventBackpressureDisconnectTotal.js +0 -4
  250. package/src/metrics/gatewayRunsCompletedTotal.js +0 -2
  251. package/src/metrics/gatewayRunsStartedTotal.js +0 -2
  252. package/src/metrics/gatewaySignalsTotal.js +0 -2
  253. package/src/metrics/gatewayWebhooksReceivedTotal.js +0 -2
  254. package/src/metrics/gatewayWebhooksRejectedTotal.js +0 -2
  255. package/src/metrics/gatewayWebhooksVerifiedTotal.js +0 -2
  256. package/src/metrics/heartbeatDataSizeBytes.js +0 -5
  257. package/src/metrics/heartbeatIntervalMs.js +0 -3
  258. package/src/metrics/hotReloadDuration.js +0 -3
  259. package/src/metrics/hotReloadFailures.js +0 -2
  260. package/src/metrics/hotReloads.js +0 -2
  261. package/src/metrics/httpRequestDuration.js +0 -3
  262. package/src/metrics/httpRequests.js +0 -2
  263. package/src/metrics/index.d.ts +0 -1222
  264. package/src/metrics/index.js +0 -168
  265. package/src/metrics/memoryFactReads.js +0 -3
  266. package/src/metrics/memoryFactWrites.js +0 -3
  267. package/src/metrics/memoryMessageSaves.js +0 -3
  268. package/src/metrics/memoryRecallDuration.js +0 -11
  269. package/src/metrics/memoryRecallQueries.js +0 -3
  270. package/src/metrics/metricsServiceAdapter.js +0 -181
  271. package/src/metrics/nodeDuration.js +0 -3
  272. package/src/metrics/nodeRetriesTotal.js +0 -2
  273. package/src/metrics/nodesFailed.js +0 -2
  274. package/src/metrics/nodesFinished.js +0 -2
  275. package/src/metrics/nodesStarted.js +0 -2
  276. package/src/metrics/openApiToolCallErrorsTotal.js +0 -3
  277. package/src/metrics/openApiToolCallsTotal.js +0 -3
  278. package/src/metrics/openApiToolDuration.js +0 -9
  279. package/src/metrics/processHeapUsedBytes.js +0 -2
  280. package/src/metrics/processMemoryRssBytes.js +0 -2
  281. package/src/metrics/processUptimeSeconds.js +0 -2
  282. package/src/metrics/promptSizeBytes.js +0 -3
  283. package/src/metrics/replaysStarted.js +0 -3
  284. package/src/metrics/responseSizeBytes.js +0 -3
  285. package/src/metrics/rewindDurationMs.js +0 -3
  286. package/src/metrics/rewindFramesDeleted.js +0 -3
  287. package/src/metrics/rewindRollbackTotal.js +0 -2
  288. package/src/metrics/rewindSandboxesReverted.js +0 -3
  289. package/src/metrics/rewindTotal.js +0 -2
  290. package/src/metrics/runDuration.js +0 -3
  291. package/src/metrics/runForksCreated.js +0 -3
  292. package/src/metrics/runsAncestryDepth.js +0 -3
  293. package/src/metrics/runsCancelledTotal.js +0 -2
  294. package/src/metrics/runsCarriedStateBytes.js +0 -5
  295. package/src/metrics/runsContinuedTotal.js +0 -2
  296. package/src/metrics/runsFailedTotal.js +0 -2
  297. package/src/metrics/runsFinishedTotal.js +0 -2
  298. package/src/metrics/runsResumedTotal.js +0 -2
  299. package/src/metrics/runsTotal.js +0 -2
  300. package/src/metrics/sandboxActive.js +0 -2
  301. package/src/metrics/sandboxBundleSizeBytes.js +0 -5
  302. package/src/metrics/sandboxCompletedTotal.js +0 -2
  303. package/src/metrics/sandboxCreatedTotal.js +0 -2
  304. package/src/metrics/sandboxDurationMs.js +0 -3
  305. package/src/metrics/sandboxPatchCount.js +0 -3
  306. package/src/metrics/sandboxTransportDurationMs.js +0 -5
  307. package/src/metrics/schedulerConcurrencyUtilization.js +0 -2
  308. package/src/metrics/schedulerQueueDepth.js +0 -2
  309. package/src/metrics/schedulerWaitDuration.js +0 -5
  310. package/src/metrics/scorerDuration.js +0 -9
  311. package/src/metrics/scorerEventsFailed.js +0 -2
  312. package/src/metrics/scorerEventsFinished.js +0 -2
  313. package/src/metrics/scorerEventsStarted.js +0 -2
  314. package/src/metrics/scorersFailed.js +0 -3
  315. package/src/metrics/scorersFinished.js +0 -3
  316. package/src/metrics/scorersStarted.js +0 -3
  317. package/src/metrics/smithersMetricCatalog.js +0 -893
  318. package/src/metrics/smithersMetricCatalogByKey.js +0 -2
  319. package/src/metrics/smithersMetricCatalogByName.js +0 -2
  320. package/src/metrics/smithersMetricCatalogByPrometheusName.js +0 -4
  321. package/src/metrics/snapshotDuration.js +0 -9
  322. package/src/metrics/snapshotsCaptured.js +0 -3
  323. package/src/metrics/supervisorPollDuration.js +0 -5
  324. package/src/metrics/supervisorPollsTotal.js +0 -2
  325. package/src/metrics/supervisorResumeLag.js +0 -5
  326. package/src/metrics/supervisorResumedTotal.js +0 -2
  327. package/src/metrics/supervisorSkippedTotal.js +0 -2
  328. package/src/metrics/supervisorStaleDetected.js +0 -2
  329. package/src/metrics/taskHeartbeatTimeoutTotal.js +0 -2
  330. package/src/metrics/taskHeartbeatsTotal.js +0 -2
  331. package/src/metrics/timerDelayDuration.js +0 -3
  332. package/src/metrics/timersCancelled.js +0 -2
  333. package/src/metrics/timersCreated.js +0 -2
  334. package/src/metrics/timersFired.js +0 -2
  335. package/src/metrics/timersPending.js +0 -2
  336. package/src/metrics/toPrometheusMetricName.js +0 -8
  337. package/src/metrics/tokensCacheReadTotal.js +0 -2
  338. package/src/metrics/tokensCacheWriteTotal.js +0 -2
  339. package/src/metrics/tokensContextWindowBucketTotal.js +0 -2
  340. package/src/metrics/tokensContextWindowPerCall.js +0 -5
  341. package/src/metrics/tokensInputPerCall.js +0 -3
  342. package/src/metrics/tokensInputTotal.js +0 -2
  343. package/src/metrics/tokensOutputPerCall.js +0 -3
  344. package/src/metrics/tokensOutputTotal.js +0 -2
  345. package/src/metrics/tokensReasoningTotal.js +0 -2
  346. package/src/metrics/toolCallErrorsTotal.js +0 -2
  347. package/src/metrics/toolCallsTotal.js +0 -2
  348. package/src/metrics/toolDuration.js +0 -3
  349. package/src/metrics/toolOutputTruncatedTotal.js +0 -2
  350. package/src/metrics/trackEvent.js +0 -608
  351. package/src/metrics/updateAsyncExternalWaitPending.js +0 -18
  352. package/src/metrics/updateProcessMetrics.js +0 -20
  353. package/src/metrics/vcsDuration.js +0 -3
  354. package/src/normalizeStructuredEvent.js +0 -15
  355. package/src/prometheusContentType.js +0 -1
  356. package/src/renderPrometheusMetrics.js +0 -212
  357. package/src/resolveAgentTraceCapabilities.js +0 -59
  358. package/src/resolveSmithersObservabilityOptions.js +0 -109
  359. package/src/smithersMetrics.js +0 -2
  360. package/src/smithersSpanNames.js +0 -6
  361. package/src/unsupportedKindsForCapabilities.js +0 -32
  362. package/src/withSmithersSpan.js +0 -17
  363. 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).