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