@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
package/src/Otlp.ts ADDED
@@ -0,0 +1,477 @@
1
+ /**
2
+ * Default OTLP export wiring for flows telemetry.
3
+ *
4
+ * The store packages already open spans through Effect's tracer and update
5
+ * `Metric` counters on their hot paths; what they deliberately do not do is
6
+ * ship an exporter. This module is that exporter: one layer that installs
7
+ * Effect's own OTLP logger, metrics exporter, and tracer
8
+ * (`effect/unstable/observability/Otlp`) against a collector endpoint, with
9
+ * the flows service identity filled in. Nothing beyond `effect` is involved;
10
+ * no OpenTelemetry SDK dependency.
11
+ *
12
+ * Browser support is met by construction rather than by a no-op variant:
13
+ * export happens over Effect's `HttpClient`, and {@link layerFetch} binds the
14
+ * `fetch` implementation the host already has, so no entry point here ever
15
+ * resolves a `node:` built-in. See the
16
+ * {@link https://smithers.sh/docs/reference/api/observability | observability API contract}.
17
+ *
18
+ * @since 0.1.0
19
+ */
20
+
21
+ import * as Redaction from "@smthrs/journal/Redaction"
22
+ import * as Cause from "effect/Cause"
23
+ import { Clock } from "effect/Clock"
24
+ import * as ConfigProvider from "effect/ConfigProvider"
25
+ import type * as Duration from "effect/Duration"
26
+ import * as Effect from "effect/Effect"
27
+ import * as Exit from "effect/Exit"
28
+ import * as Layer from "effect/Layer"
29
+ import * as Logger from "effect/Logger"
30
+ import * as Metric from "effect/Metric"
31
+ import * as Option from "effect/Option"
32
+ import * as References from "effect/References"
33
+ import * as Schema from "effect/Schema"
34
+ import * as Semaphore from "effect/Semaphore"
35
+ import * as Tracer from "effect/Tracer"
36
+ import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"
37
+ import * as Headers from "effect/unstable/http/Headers"
38
+ import type * as HttpBody from "effect/unstable/http/HttpBody"
39
+ import * as HttpClient from "effect/unstable/http/HttpClient"
40
+ import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"
41
+ import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
42
+ import * as Otlp from "effect/unstable/observability/Otlp"
43
+ import * as Endpoint from "./Endpoint.ts"
44
+ import * as Resource from "./Resource.ts"
45
+
46
+ /**
47
+ * The `service.name` resource attribute installed when the caller supplies
48
+ * none: the flows distribution itself. Override it per application with
49
+ * {@link Options.serviceName}.
50
+ *
51
+ * @category resource
52
+ * @since 0.1.0
53
+ */
54
+ export const defaultServiceName = "flows"
55
+
56
+ /**
57
+ * The `service.version` resource attribute installed when the caller supplies
58
+ * none. Mirrors the release version in this package's `package.json`.
59
+ *
60
+ * A published package cannot read its own manifest on every runtime it
61
+ * supports, so the version lives here as a literal.
62
+ * `scripts/set-release-version.mjs` rewrites this declaration with the
63
+ * manifests, and its `--check` mode reports drift, so a release bump cannot
64
+ * leave it behind.
65
+ *
66
+ * @category resource
67
+ * @since 0.1.0
68
+ */
69
+ export const defaultServiceVersion = "1.0.0-rc.3"
70
+
71
+ // The upstream logger and tracer each buffer at most one 1,000-record batch.
72
+ // Do not queue at the transport: upstream forks every full batch independently.
73
+ const maxBatchSize = 1000
74
+ const maxInFlight = 4
75
+ /**
76
+ * Maximum encoded export request size, in bytes.
77
+ * @category transport
78
+ * @since 1.0.0-rc.0
79
+ */
80
+ export const maxRequestBytes = 1024 * 1024
81
+ const requestTimeout = "10 seconds"
82
+ const diagnosticIntervalMillis = 60_000
83
+ // The envelope around the resource in every request repeats `service.name` as
84
+ // the instrumentation scope name: at most 1,024 code units, at most six bytes
85
+ // each once JSON-escaped, so under 8 KiB together with the envelope's own keys.
86
+ /**
87
+ * Reserved bytes for request wrappers and the escaped instrumentation scope.
88
+ * @category transport
89
+ * @since 1.0.0-rc.0
90
+ */
91
+ export const maximumEnvelopeBytes = 8 * 1024
92
+
93
+ /**
94
+ * Bytes of every export request kept free for the signal batch itself.
95
+ *
96
+ * The transport discards a request over 1 MiB, and the resource rides in every
97
+ * request, so the largest resource `Resource.decode` admits
98
+ * (`Resource.maximumResourceBytes`, 128 KiB) and the envelope that repeats the
99
+ * service name (under 8 KiB) come off the top. The 888 KiB left gives each
100
+ * record of a full 1,000-record logger or tracer batch about 900 bytes, three
101
+ * to four times a bare log record or span, and gives the metrics snapshot room
102
+ * for several thousand series. Every resource the decoder accepts therefore
103
+ * leaves an ordinary batch of any signal exportable; what this reserve does
104
+ * not bound is a single application record or a registry large enough to
105
+ * exceed it on its own.
106
+ *
107
+ * @category transport
108
+ * @since 1.0.0-rc.0
109
+ */
110
+ export const reservedBatchBytes = maxRequestBytes - Resource.maximumResourceBytes - maximumEnvelopeBytes
111
+
112
+ type DiscardReason = "oversized" | "stalled" | "saturated"
113
+
114
+ const boundedClient = Layer.effect(
115
+ HttpClient.HttpClient,
116
+ Effect.gen(function*() {
117
+ const client = yield* HttpClient.HttpClient
118
+ const clock = yield* Clock
119
+ // The loggers installed when this transport is acquired. The OTLP logger
120
+ // the same acquisition installs does not exist yet, so a diagnostic pinned
121
+ // to this set cannot enter the exporter it describes, whichever fiber runs
122
+ // the export: the interval and shutdown fibers inherit this set anyway,
123
+ // but a flush requested from application code runs with the application's
124
+ // loggers, which by then include this exporter.
125
+ const ambientLoggers = yield* Effect.withFiber((fiber) => Effect.succeed(fiber.getRef(Logger.CurrentLoggers)))
126
+ const permits = Semaphore.makeUnsafe(maxInFlight)
127
+ // The counter lives with the transport it describes.
128
+ const droppedExports = Metric.counter("flows_observability_otlp_dropped")
129
+ let nextDiagnosticAt = -Infinity
130
+ // A local discard is terminal, so the upstream retry loop must not retain
131
+ // or retry its payload, and the counter records batches, not records. The
132
+ // warning is the diagnostic the counter cannot be: the counter is exported
133
+ // through this same transport and shares every outage with it. One
134
+ // warning per minute keeps a sustained outage from flooding the ambient
135
+ // loggers; the running total in its annotations carries the volume.
136
+ // Replace ambient annotations and log spans to bound the diagnostic itself.
137
+ const discard = (reason: DiscardReason, request: HttpClientRequest.HttpClientRequest, bytes: number) =>
138
+ Metric.update(droppedExports, 1).pipe(
139
+ Effect.andThen(Effect.suspend(() => {
140
+ const now = clock.currentTimeMillisUnsafe()
141
+ if (now < nextDiagnosticAt) return Effect.void
142
+ nextDiagnosticAt = now + diagnosticIntervalMillis
143
+ return Metric.value(droppedExports).pipe(
144
+ Effect.flatMap((dropped) =>
145
+ Effect.logWarning("An OTLP export batch was discarded").pipe(
146
+ Effect.provideService(References.CurrentLogSpans, []),
147
+ Effect.provideService(References.CurrentLogAnnotations, {
148
+ code: "otlp_export_discarded",
149
+ reason,
150
+ bytes,
151
+ limit: maxRequestBytes,
152
+ dropped: dropped.count
153
+ })
154
+ )
155
+ ),
156
+ Effect.provideService(Logger.CurrentLoggers, ambientLoggers)
157
+ )
158
+ })),
159
+ Effect.as(HttpClientResponse.fromWeb(request, new Response(null, { status: 204 })))
160
+ )
161
+ return HttpClient.transform(client, (requestEffect, request) =>
162
+ Effect.suspend(() => {
163
+ // layerJson always supplies a Uint8Array body with a byte length.
164
+ const bytes = (request.body as HttpBody.Uint8Array).contentLength
165
+ if (bytes > maxRequestBytes) return discard("oversized", request, bytes)
166
+ // Fetch must expose redirects before any credential or signal body
167
+ // leaves the configured endpoint, even when the caller enables follow.
168
+ const confinedRequest = Effect.flatMap(Effect.serviceOption(FetchHttpClient.RequestInit), (init) =>
169
+ requestEffect.pipe(
170
+ Effect.provideService(FetchHttpClient.RequestInit, {
171
+ ...Option.getOrUndefined(init),
172
+ redirect: "manual"
173
+ })
174
+ ))
175
+ return confinedRequest.pipe(
176
+ Effect.timeoutOrElse({
177
+ duration: requestTimeout,
178
+ orElse: () =>
179
+ discard("stalled", request, bytes)
180
+ }),
181
+ permits.withPermitsIfAvailable(1),
182
+ Effect.flatMap(Option.match({
183
+ onNone: () =>
184
+ discard("saturated", request, bytes),
185
+ onSome: Effect.succeed
186
+ }))
187
+ )
188
+ }))
189
+ })
190
+ )
191
+
192
+ const loopbackIpv4 = /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
193
+
194
+ /** Whether a decoded endpoint's host never leaves this machine. */
195
+ const isLoopback = (endpoint: string): boolean => {
196
+ const hostname = new URL(endpoint).hostname
197
+ return hostname === "localhost" || hostname === "[::1]" || loopbackIpv4.test(hostname)
198
+ }
199
+
200
+ /**
201
+ * Refuses a credential header bound for a plaintext collector off this
202
+ * machine. Anyone on the network path could read the token from every export
203
+ * request, and the exporter would never report it. Loopback `http://` stays
204
+ * allowed for a local collector or sidecar; non-credential headers such as a
205
+ * tenant id stay allowed on any endpoint.
206
+ */
207
+ /**
208
+ * Vendor credential headers whose names `Redaction.isSensitiveKey` does not
209
+ * recognize. Honeycomb sends its ingest key as `x-honeycomb-team`.
210
+ */
211
+ const vendorCredentialHeaders: ReadonlySet<string> = new Set(["x-honeycomb-team"])
212
+
213
+ const isCredentialHeader = (name: string): boolean =>
214
+ Redaction.isSensitiveKey(name) || vendorCredentialHeaders.has(name.toLowerCase())
215
+
216
+ const refusePlaintextCredentials = (
217
+ baseUrl: string,
218
+ headers: Headers.Input | undefined
219
+ ): Effect.Effect<void, Endpoint.InvalidExporterEndpoint> => {
220
+ if (!baseUrl.toLowerCase().startsWith("http:") || isLoopback(baseUrl)) return Effect.void
221
+ const credential = Object.keys(Headers.fromInput(headers)).some(isCredentialHeader)
222
+ return credential
223
+ ? Effect.fail(
224
+ new Endpoint.InvalidExporterEndpoint({
225
+ code: "invalid_exporter_endpoint",
226
+ path: "baseUrl",
227
+ message:
228
+ "OTLP collector baseUrl must use https:// when headers carry credentials, unless it is a loopback address"
229
+ })
230
+ )
231
+ : Effect.void
232
+ }
233
+
234
+ const redactText = (text: string): string => String(Redaction.redact(text))
235
+
236
+ /**
237
+ * A failed span's error rebuilt with its text redacted. It keeps the original
238
+ * error's `name` and `stack` (redacted) so the collector's exception type and
239
+ * trace stay readable; it is exported to the collector and never thrown.
240
+ */
241
+ class RedactedSpanError extends Schema.TaggedError<RedactedSpanError>()("@smthrs/observability/RedactedSpanError", {
242
+ message: Schema.String
243
+ }) {}
244
+
245
+ /**
246
+ * A span failure rendered with credentials removed. The exporter serializes a
247
+ * failed span's cause into its status message and `exception` events, which
248
+ * would otherwise carry any token an error message quoted to the collector.
249
+ */
250
+ const redactedExit = (exit: Exit.Exit<unknown, unknown>): Exit.Exit<unknown, unknown> => {
251
+ if (exit._tag === "Success" || Cause.hasInterruptsOnly(exit.cause)) return exit
252
+ const errors = Cause.prettyErrors(exit.cause, { includeCauseInStack: true }).map((error) => {
253
+ const redacted = new RedactedSpanError({ message: redactText(error.message) })
254
+ Object.defineProperty(redacted, "name", { value: redactText(error.name), configurable: true, writable: true })
255
+ // `prettyErrors` always renders a stack.
256
+ redacted.stack = redactText(String(error.stack))
257
+ return redacted
258
+ })
259
+ return Exit.failCause(errors.map(Cause.fail).reduce(Cause.combine, Cause.empty as Cause.Cause<unknown>))
260
+ }
261
+
262
+ /**
263
+ * Wraps the installed tracer so every exported span's failure and string
264
+ * attributes pass through the journal redaction rules first.
265
+ */
266
+ const redactedSpans = Layer.effect(Tracer.Tracer)(
267
+ Effect.map(Effect.tracer, (tracer) =>
268
+ Tracer.make({
269
+ ...tracer,
270
+ span(options) {
271
+ const span = tracer.span(options)
272
+ const end = span.end.bind(span)
273
+ const attribute = span.attribute.bind(span)
274
+ span.end = (endTime, exit) => end(endTime, redactedExit(exit))
275
+ span.attribute = (key, value) => attribute(key, typeof value === "string" ? redactText(value) : value)
276
+ return span
277
+ }
278
+ }))
279
+ )
280
+
281
+ /**
282
+ * Configuration for the default OTLP wiring.
283
+ *
284
+ * @category models
285
+ * @since 0.1.0
286
+ */
287
+ export interface Options {
288
+ /**
289
+ * The collector base URL, for example `http://localhost:4318`. Signals are
290
+ * posted below it at `/v1/logs`, `/v1/metrics`, and `/v1/traces`. It must be
291
+ * an absolute `http://` or `https://` URL without credentials, a query,
292
+ * fragment, backslashes, spaces, or controls. A base path is allowed. Invalid
293
+ * values fail acquisition with {@link Endpoint.InvalidExporterEndpoint}.
294
+ * Use {@link Options.headers} for authentication.
295
+ */
296
+ readonly baseUrl: string
297
+ /** Overrides {@link defaultServiceName} as the `service.name` attribute. */
298
+ readonly serviceName?: string | undefined
299
+ /** Overrides {@link defaultServiceVersion} as the `service.version` attribute. */
300
+ readonly serviceVersion?: string | undefined
301
+ /**
302
+ * Additional resource attributes attached to every exported signal, decoded
303
+ * by `Resource.Attributes`. The whole resource must encode to at most
304
+ * `Resource.maximumResourceBytes`; a larger one fails acquisition with
305
+ * {@link Resource.InvalidResourceConfiguration} rather than making every
306
+ * request oversized.
307
+ */
308
+ readonly attributes?: Record<string, unknown> | undefined
309
+ /**
310
+ * Headers sent with every export request, for example vendor auth. A header
311
+ * whose name names a credential (`authorization`, `x-api-key`, anything
312
+ * `Redaction.isSensitiveKey` recognizes, plus `x-honeycomb-team`) requires
313
+ * an `https://` baseUrl unless the host is loopback; otherwise acquisition
314
+ * fails with
315
+ * {@link Endpoint.InvalidExporterEndpoint}.
316
+ */
317
+ readonly headers?: Headers.Input | undefined
318
+ /** Export cadence for all three signals; each signal's Effect default applies when omitted. */
319
+ readonly exportInterval?: Duration.Input | undefined
320
+ /** Upper bound on the shutdown flush when the layer's scope closes. */
321
+ readonly shutdownTimeout?: Duration.Input | undefined
322
+ }
323
+
324
+ /**
325
+ * Creates the OTLP logs, metrics, and traces layer with flows resource
326
+ * defaults, JSON-serialized. Exports share a four-request limit with no waiting
327
+ * queue. Requests larger than 1 MiB or stalled for ten seconds are discarded;
328
+ * `flows_observability_otlp_dropped` counts discarded batches, and at most
329
+ * once a minute a `Warn` record with code `otlp_export_discarded` names the
330
+ * reason, the request size, and the running total through the loggers that
331
+ * were installed when the layer was acquired, never through this exporter.
332
+ *
333
+ * **Details**
334
+ *
335
+ * The layer still requires an `HttpClient`, which is how it stays
336
+ * platform-neutral: a Node host may hand it `@effect/platform-node`'s Undici
337
+ * client (re-exported by `@smthrs/platform-node`), a browser or test
338
+ * hands it something else. Use {@link layerFetch} when the host's global
339
+ * `fetch` is good enough. On Node 26 and every browser it is.
340
+ * Fetch redirects are refused. Supply `FetchHttpClient.RequestInit` around
341
+ * the whole exporting effect to retain other fetch options; options scoped
342
+ * only to the supplied client's layer are replaced by the redirect policy.
343
+ * Other HTTP transports must independently refuse automatic redirects.
344
+ *
345
+ * @category layers
346
+ * @since 0.1.0
347
+ */
348
+ export const layer = (
349
+ options: Options
350
+ ): Layer.Layer<
351
+ never,
352
+ Resource.InvalidResourceConfiguration | Endpoint.InvalidExporterEndpoint,
353
+ HttpClient.HttpClient
354
+ > =>
355
+ Layer.unwrap(
356
+ Effect.map(
357
+ Effect.all([
358
+ Resource.decode({
359
+ serviceName: options.serviceName ?? defaultServiceName,
360
+ serviceVersion: options.serviceVersion ?? defaultServiceVersion,
361
+ ...(options.attributes === undefined ? {} : { attributes: options.attributes })
362
+ }),
363
+ Endpoint.decode(options.baseUrl, "baseUrl").pipe(
364
+ Effect.tap((baseUrl) => refusePlaintextCredentials(baseUrl, options.headers))
365
+ )
366
+ ]),
367
+ ([decoded, baseUrl]) => {
368
+ const resource = Resource.toOpenTelemetryConfiguration(decoded)
369
+ return (
370
+ Otlp.layerJson({
371
+ baseUrl,
372
+ resource: {
373
+ serviceName: resource.serviceName,
374
+ // `serviceVersion` is supplied above before Resource decoding.
375
+ serviceVersion: resource.serviceVersion!,
376
+ ...(resource.attributes === undefined ? {} : { attributes: resource.attributes })
377
+ },
378
+ headers: options.headers,
379
+ maxBatchSize,
380
+ loggerExportInterval: options.exportInterval,
381
+ metricsExportInterval: options.exportInterval,
382
+ tracerExportInterval: options.exportInterval,
383
+ shutdownTimeout: options.shutdownTimeout
384
+ }).pipe(
385
+ Layer.provide(boundedClient),
386
+ // Upstream merges OTEL_RESOURCE_ATTRIBUTES even with explicit
387
+ // options. Keep unvalidated ambient metadata out of every request.
388
+ Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown({}))),
389
+ (exporter) => Layer.provideMerge(redactedSpans, exporter)
390
+ )
391
+ )
392
+ }
393
+ )
394
+ )
395
+
396
+ /**
397
+ * Provides {@link layer} over the host's global `fetch`, the default wiring
398
+ * for a Node host, and browser-safe by construction because it never touches
399
+ * a `node:` built-in.
400
+ *
401
+ * **Example**
402
+ *
403
+ * ```ts
404
+ * import * as Otlp from "@smthrs/observability/Otlp"
405
+ *
406
+ * const Telemetry = Otlp.layerFetch({ baseUrl: "http://localhost:4318" })
407
+ * ```
408
+ *
409
+ * @category layers
410
+ * @since 0.1.0
411
+ */
412
+ export const layerFetch = (
413
+ options: Options
414
+ ): Layer.Layer<never, Resource.InvalidResourceConfiguration | Endpoint.InvalidExporterEndpoint> =>
415
+ layer(options).pipe(Layer.provide(FetchHttpClient.layer))
416
+
417
+ /**
418
+ * Why an OTLP layer refused to start.
419
+ *
420
+ * @category errors
421
+ * @since 1.0.0
422
+ */
423
+ export type LayerError = Resource.InvalidResourceConfiguration | Endpoint.InvalidExporterEndpoint
424
+
425
+ /**
426
+ * Parses `OTEL_EXPORTER_OTLP_HEADERS`: comma-separated `key=value` pairs whose
427
+ * values are percent-decoded, as the OpenTelemetry exporter specification
428
+ * defines. A pair without `=` or with an empty key is skipped.
429
+ *
430
+ * @category environment
431
+ * @since 1.0.0
432
+ */
433
+ export const parseHeaders = (value: string | undefined): Record<string, string> => {
434
+ const headers: Record<string, string> = {}
435
+ for (const pair of (value ?? "").split(",")) {
436
+ const separator = pair.indexOf("=")
437
+ const key = pair.slice(0, separator).trim()
438
+ if (separator < 0 || key === "") continue
439
+ const raw = pair.slice(separator + 1).trim()
440
+ let decoded = raw
441
+ try {
442
+ decoded = decodeURIComponent(raw)
443
+ } catch { /* A malformed escape is kept verbatim. */ }
444
+ headers[key] = decoded
445
+ }
446
+ return headers
447
+ }
448
+
449
+ /**
450
+ * {@link layerFetch} when the environment names a collector, otherwise
451
+ * {@link layerNoop}.
452
+ *
453
+ * `OTEL_EXPORTER_OTLP_ENDPOINT` is the collector base URL and
454
+ * `OTEL_EXPORTER_OTLP_HEADERS` the headers sent with every export. An unset or
455
+ * empty endpoint installs no exporter at all.
456
+ *
457
+ * @category layers
458
+ * @since 1.0.0
459
+ */
460
+ export const layerEnvironment = (
461
+ environment: Readonly<Record<string, string | undefined>>,
462
+ options: Omit<Options, "baseUrl" | "headers"> = {}
463
+ ): Layer.Layer<never, LayerError> => {
464
+ const endpoint = environment["OTEL_EXPORTER_OTLP_ENDPOINT"]?.trim()
465
+ if (endpoint === undefined || endpoint === "") return layerNoop
466
+ return layerFetch({ ...options, baseUrl: endpoint, headers: parseHeaders(environment["OTEL_EXPORTER_OTLP_HEADERS"]) })
467
+ }
468
+
469
+ /**
470
+ * Exports nothing. The explicit stand-in for hosts with no collector, such as
471
+ * a development shell, a test, or a browser deployment that has not opted in, so
472
+ * wiring code can switch layers rather than branch.
473
+ *
474
+ * @category layers
475
+ * @since 0.1.0
476
+ */
477
+ export const layerNoop: Layer.Layer<never> = Layer.empty