@smthrs/observability 0.35.0 → 1.0.0-rc.4

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,596 @@
1
+ /**
2
+ * Non-blocking forwarding of bounded operational logs to the durable journal.
3
+ *
4
+ * @since 0.1.0
5
+ */
6
+
7
+ import * as Journal from "@smthrs/journal/Journal"
8
+ import * as JournalEvent from "@smthrs/journal/JournalEvent"
9
+ import * as Redaction from "@smthrs/journal/Redaction"
10
+ import * as Cause from "effect/Cause"
11
+ import * as Context from "effect/Context"
12
+ import * as Effect from "effect/Effect"
13
+ import * as Layer from "effect/Layer"
14
+ import * as Logger from "effect/Logger"
15
+ import type * as LogLevel from "effect/LogLevel"
16
+ import * as EffectMetric from "effect/Metric"
17
+ import * as Option from "effect/Option"
18
+ import * as Queue from "effect/Queue"
19
+ import * as References from "effect/References"
20
+ import * as Schema from "effect/Schema"
21
+ import * as Tracer from "effect/Tracer"
22
+ import { schemaIssuePath } from "./internal/schemaIssuePath.ts"
23
+ import { droppedLogRecords } from "./Metric.ts"
24
+
25
+ /**
26
+ * Largest queue accepted by the forwarding logger.
27
+ *
28
+ * @category constants
29
+ * @since 1.0.0-rc.0
30
+ */
31
+ export const maximumCapacity = 65_536
32
+
33
+ /**
34
+ * Largest detached log payload admitted to the asynchronous queue.
35
+ *
36
+ * @category constants
37
+ * @since 1.0.0-rc.0
38
+ */
39
+ export const maximumSnapshotBytes = 1024 * 1024
40
+
41
+ /**
42
+ * Largest number of container members copied into one detached log payload.
43
+ *
44
+ * @category constants
45
+ * @since 1.0.0-rc.0
46
+ */
47
+ export const maximumSnapshotMembers = 4_096
48
+
49
+ /**
50
+ * Deepest container path copied into one detached log payload.
51
+ *
52
+ * @category constants
53
+ * @since 1.0.0-rc.0
54
+ */
55
+ export const maximumSnapshotDepth = 64
56
+
57
+ /**
58
+ * What a hostile or unreadable logged value becomes.
59
+ *
60
+ * @category constants
61
+ * @since 1.0.0-rc.0
62
+ */
63
+ export const unrenderableMarker = "[Unrenderable]"
64
+
65
+ /**
66
+ * What a value beyond a logger resource ceiling becomes.
67
+ *
68
+ * @category constants
69
+ * @since 1.0.0-rc.0
70
+ */
71
+ export const truncatedMarker = "[Truncated]"
72
+
73
+ /**
74
+ * Options for the journal forwarding logger.
75
+ *
76
+ * @category models
77
+ * @since 1.0.0-rc.0
78
+ */
79
+ export interface Options {
80
+ /** The run every forwarded record is attributed to. */
81
+ readonly runId: JournalEvent.RunId
82
+ /** Queue depth, 1 through {@link maximumCapacity}. Defaults to 256. */
83
+ readonly capacity?: number | undefined
84
+ /**
85
+ * Pins `References.MinimumLogLevel` for the whole application. Omitted, the
86
+ * layer leaves that reference alone and Effect's own `Info` default applies.
87
+ */
88
+ readonly minimumLogLevel?: LogLevel.LogLevel | undefined
89
+ /**
90
+ * Keeps the ambient loggers alongside the forwarder. Defaults to `false`,
91
+ * which replaces the ambient logger set.
92
+ */
93
+ readonly mergeWithExisting?: boolean | undefined
94
+ }
95
+
96
+ /**
97
+ * A stable logger configuration refusal.
98
+ *
99
+ * @category errors
100
+ * @since 1.0.0-rc.0
101
+ */
102
+ export class InvalidJournalLoggerOptions extends Schema.TaggedError<InvalidJournalLoggerOptions>()(
103
+ "@smthrs/observability/InvalidJournalLoggerOptions",
104
+ {
105
+ code: Schema.Literal("invalid_journal_logger_options"),
106
+ path: Schema.String,
107
+ message: Schema.String
108
+ }
109
+ ) {}
110
+
111
+ /**
112
+ * One typed failure retained from an Effect cause.
113
+ *
114
+ * @category schemas
115
+ * @since 1.0.0-rc.0
116
+ */
117
+ export const TelemetryFail = Schema.Struct({
118
+ _tag: Schema.Literal("Fail"),
119
+ error: Schema.Unknown
120
+ })
121
+
122
+ /**
123
+ * One unexpected defect retained from an Effect cause.
124
+ *
125
+ * @category schemas
126
+ * @since 1.0.0-rc.0
127
+ */
128
+ export const TelemetryDie = Schema.Struct({
129
+ _tag: Schema.Literal("Die"),
130
+ defect: Schema.Unknown
131
+ })
132
+
133
+ /**
134
+ * One interruption retained from an Effect cause.
135
+ *
136
+ * @category schemas
137
+ * @since 1.0.0-rc.0
138
+ */
139
+ export const TelemetryInterrupt = Schema.Struct({
140
+ _tag: Schema.Literal("Interrupt"),
141
+ fiberId: Schema.NullOr(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)))
142
+ })
143
+
144
+ /**
145
+ * One reason in a persisted telemetry cause.
146
+ *
147
+ * @category schemas
148
+ * @since 1.0.0-rc.0
149
+ */
150
+ export const TelemetryCauseReason = Schema.Union([TelemetryFail, TelemetryDie, TelemetryInterrupt])
151
+
152
+ /**
153
+ * One reason in a persisted telemetry cause.
154
+ *
155
+ * @category models
156
+ * @since 1.0.0-rc.0
157
+ */
158
+ export type TelemetryCauseReason = typeof TelemetryCauseReason.Type
159
+
160
+ /**
161
+ * A versioned structural Effect cause.
162
+ *
163
+ * Effect 4 stores composed failures as an ordered reason list. The projection
164
+ * preserves that order and each fail, defect, or interrupt discriminator.
165
+ *
166
+ * @category schemas
167
+ * @since 1.0.0-rc.0
168
+ */
169
+ export const TelemetryCause = Schema.Struct({
170
+ version: Schema.Literal(1),
171
+ reasons: Schema.Array(TelemetryCauseReason)
172
+ })
173
+
174
+ /**
175
+ * A versioned structural Effect cause.
176
+ *
177
+ * @category models
178
+ * @since 1.0.0-rc.0
179
+ */
180
+ export type TelemetryCause = typeof TelemetryCause.Type
181
+
182
+ /**
183
+ * Runtime schema for the operational log payload written to the journal.
184
+ *
185
+ * @category schemas
186
+ * @since 1.0.0-rc.0
187
+ */
188
+ export const TelemetryLog = Schema.Struct({
189
+ version: Schema.Literal(1),
190
+ level: Schema.Literals(["All", "Fatal", "Error", "Warn", "Info", "Debug", "Trace", "None"]),
191
+ message: Schema.Unknown,
192
+ annotations: Schema.Record(Schema.String, Schema.Unknown),
193
+ cause: TelemetryCause,
194
+ fiberId: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
195
+ traceId: Schema.optionalKey(Schema.String),
196
+ spanId: Schema.optionalKey(Schema.String),
197
+ timestamp: Schema.String
198
+ })
199
+
200
+ /**
201
+ * Versioned operational log payload written to the journal.
202
+ *
203
+ * @category models
204
+ * @since 1.0.0-rc.0
205
+ */
206
+ export type TelemetryLog = typeof TelemetryLog.Type
207
+
208
+ const sourceId = "flows/observability/logger" as JournalEvent.SourceId
209
+ const encoder = new TextEncoder()
210
+
211
+ const OptionsSchema = Schema.Struct({
212
+ runId: JournalEvent.RunId,
213
+ capacity: Schema.optional(
214
+ Schema.Int.check(
215
+ Schema.isGreaterThanOrEqualTo(1),
216
+ Schema.isLessThanOrEqualTo(maximumCapacity)
217
+ )
218
+ ),
219
+ minimumLogLevel: Schema.optional(
220
+ Schema.Literals(["All", "Fatal", "Error", "Warn", "Info", "Debug", "Trace", "None"])
221
+ ),
222
+ mergeWithExisting: Schema.optional(Schema.Boolean)
223
+ })
224
+
225
+ const validateOptions = (options: Options) =>
226
+ Schema.decodeUnknownEffect(OptionsSchema)(options).pipe(
227
+ Effect.mapError((cause) => {
228
+ const path = schemaIssuePath(cause, "options")
229
+ return new InvalidJournalLoggerOptions({
230
+ code: "invalid_journal_logger_options",
231
+ path,
232
+ message: `journal logger ${path} is invalid`
233
+ })
234
+ })
235
+ )
236
+
237
+ interface SnapshotBudget {
238
+ bytes: number
239
+ members: number
240
+ }
241
+
242
+ const reserve = (budget: SnapshotBudget, bytes: number): boolean => {
243
+ if (bytes > budget.bytes) return false
244
+ budget.bytes -= bytes
245
+ return true
246
+ }
247
+
248
+ const textBytes = (value: string): number => encoder.encode(JSON.stringify(value)).byteLength
249
+
250
+ const quoteBytes = 2
251
+ const shortEscapes = new Set([0x08, 0x09, 0x0a, 0x0c, 0x0d])
252
+
253
+ /** Bytes one non-surrogate code unit occupies inside a JSON string body. */
254
+ const unitBytes = (code: number): number => {
255
+ if (code === 0x22 || code === 0x5c) return 2
256
+ if (code < 0x20) return shortEscapes.has(code) ? 2 : 6
257
+ if (code < 0x80) return 1
258
+ if (code < 0x800) return 2
259
+ return 3
260
+ }
261
+
262
+ const isLowSurrogate = (code: number): boolean => code >= 0xdc00 && code <= 0xdfff
263
+
264
+ /**
265
+ * The longest prefix whose JSON string body fits `limit` bytes, measured in one
266
+ * forward pass. A surrogate pair is admitted or refused whole, so the retained
267
+ * text is well formed whenever the input is.
268
+ */
269
+ const boundedPrefix = (value: string, limit: number): { readonly text: string; readonly bytes: number } => {
270
+ let bytes = 0
271
+ let index = 0
272
+ while (index < value.length) {
273
+ const code = value.charCodeAt(index)
274
+ let width = 1
275
+ let cost: number
276
+ if (code >= 0xd800 && code <= 0xdbff && isLowSurrogate(value.charCodeAt(index + 1))) {
277
+ width = 2
278
+ cost = 4
279
+ } else if (code >= 0xd800 && code <= 0xdfff) {
280
+ cost = 6
281
+ } else {
282
+ cost = unitBytes(code)
283
+ }
284
+ if (bytes + cost > limit) break
285
+ bytes += cost
286
+ index += width
287
+ }
288
+ return { text: value.slice(0, index), bytes }
289
+ }
290
+
291
+ const boundedText = (value: string, budget: SnapshotBudget): string => {
292
+ if (value.length <= maximumSnapshotBytes && reserve(budget, textBytes(value))) return value
293
+ const markerBytes = textBytes(truncatedMarker)
294
+ const prefix = boundedPrefix(value, Math.max(0, budget.bytes - markerBytes - quoteBytes))
295
+ budget.bytes = Math.max(0, budget.bytes - (prefix.bytes + quoteBytes) - markerBytes)
296
+ return `${prefix.text}${truncatedMarker}`
297
+ }
298
+
299
+ /**
300
+ * Error fields read through the prototype chain, in the order the snapshot
301
+ * projects them before any own data property the error added.
302
+ */
303
+ const standardErrorKeys = ["name", "message", "stack", "cause"] as const
304
+
305
+ const snapshotValue = (
306
+ value: unknown,
307
+ budget: SnapshotBudget,
308
+ ancestors: WeakSet<object>,
309
+ depth = 0
310
+ ): unknown => {
311
+ if (budget.bytes <= 0) return truncatedMarker
312
+ if (depth > maximumSnapshotDepth) return Redaction.depthMarker
313
+ if (typeof value === "string") return boundedText(value, budget)
314
+ if (typeof value === "number") {
315
+ const projected = Number.isFinite(value) ? value : String(value)
316
+ reserve(budget, String(projected).length)
317
+ return projected
318
+ }
319
+ if (typeof value === "boolean" || value === null) {
320
+ reserve(budget, value === null ? 4 : value ? 4 : 5)
321
+ return value
322
+ }
323
+ if (typeof value === "bigint") return boundedText(`${value}n`, budget)
324
+ if (value === undefined) return boundedText("[Undefined]", budget)
325
+ if (typeof value === "function") return boundedText(Redaction.functionMarker, budget)
326
+ if (typeof value === "symbol") return boundedText(Redaction.symbolMarker, budget)
327
+ try {
328
+ if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
329
+ return boundedText(Redaction.binaryMarker, budget)
330
+ }
331
+ } catch {
332
+ return boundedText(unrenderableMarker, budget)
333
+ }
334
+ if (ancestors.has(value)) return boundedText("[Circular]", budget)
335
+ ancestors.add(value)
336
+ try {
337
+ if (value instanceof Date) {
338
+ try {
339
+ return boundedText(value.toISOString(), budget)
340
+ } catch {
341
+ return boundedText(unrenderableMarker, budget)
342
+ }
343
+ }
344
+ if (value instanceof Error) {
345
+ const record: Record<string, unknown> = Object.create(null)
346
+ for (const key of standardErrorKeys.slice(0, budget.members)) {
347
+ budget.members--
348
+ try {
349
+ record[key] = snapshotValue(value[key], budget, ancestors, depth + 1)
350
+ } catch {
351
+ record[key] = unrenderableMarker
352
+ }
353
+ }
354
+ // Tagged failures (`_tag`, domain fields) and system errors (`code`,
355
+ // `path`, `syscall`) keep their diagnostics as own data properties, so
356
+ // the walk that projects plain records runs over the remaining keys.
357
+ let names: ReadonlyArray<string>
358
+ try {
359
+ names = Object.getOwnPropertyNames(value).filter((key) =>
360
+ !(standardErrorKeys as ReadonlyArray<string>).includes(key)
361
+ )
362
+ } catch {
363
+ return record
364
+ }
365
+ let projected = 0
366
+ let collided = false
367
+ for (; projected < names.length && budget.members > 0; projected++) {
368
+ budget.members--
369
+ const key = names[projected]!
370
+ const boundedKey = boundedText(key, budget)
371
+ if (Object.hasOwn(record, boundedKey)) {
372
+ collided = true
373
+ continue
374
+ }
375
+ const descriptor = Object.getOwnPropertyDescriptor(value, key)
376
+ record[boundedKey] = descriptor !== undefined && "value" in descriptor
377
+ ? snapshotValue(descriptor.value, budget, ancestors, depth + 1)
378
+ : unrenderableMarker
379
+ }
380
+ if (collided || projected < names.length) record[truncatedMarker] = truncatedMarker
381
+ return record
382
+ }
383
+ if (Array.isArray(value)) {
384
+ const output: Array<unknown> = []
385
+ const length = value.length
386
+ for (let index = 0; index < length && budget.members > 0; index++) {
387
+ budget.members--
388
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index))
389
+ output.push(
390
+ descriptor === undefined || !("value" in descriptor)
391
+ ? unrenderableMarker
392
+ : snapshotValue(descriptor.value, budget, ancestors, depth + 1)
393
+ )
394
+ }
395
+ if (output.length < length) output.push(truncatedMarker)
396
+ return output
397
+ }
398
+ const record: Record<string, unknown> = Object.create(null)
399
+ const names = Object.getOwnPropertyNames(value)
400
+ for (let index = 0; index < names.length && budget.members > 0; index++) {
401
+ budget.members--
402
+ const key = names[index]!
403
+ const descriptor = Object.getOwnPropertyDescriptor(value, key)
404
+ const boundedKey = boundedText(key, budget)
405
+ // A key truncated to fit the budget can read exactly like one already
406
+ // copied. The first member keeps its slot; the later one is dropped and
407
+ // the record is marked truncated below rather than silently overwritten.
408
+ if (Object.hasOwn(record, boundedKey)) continue
409
+ record[boundedKey] = descriptor !== undefined && "value" in descriptor
410
+ ? snapshotValue(descriptor.value, budget, ancestors, depth + 1)
411
+ : unrenderableMarker
412
+ }
413
+ if (Object.keys(record).length < names.length) record[truncatedMarker] = truncatedMarker
414
+ return record
415
+ } catch {
416
+ return boundedText(unrenderableMarker, budget)
417
+ } finally {
418
+ ancestors.delete(value)
419
+ }
420
+ }
421
+
422
+ const snapshotCause = (cause: Cause.Cause<unknown>, budget: SnapshotBudget): TelemetryCause => ({
423
+ version: 1,
424
+ reasons: cause.reasons.slice(0, maximumSnapshotMembers).map((reason): TelemetryCauseReason => {
425
+ switch (reason._tag) {
426
+ case "Fail":
427
+ return { _tag: "Fail", error: snapshotValue(reason.error, budget, new WeakSet()) }
428
+ case "Die":
429
+ return { _tag: "Die", defect: snapshotValue(reason.defect, budget, new WeakSet()) }
430
+ case "Interrupt":
431
+ return { _tag: "Interrupt", fiberId: reason.fiberId ?? null }
432
+ }
433
+ })
434
+ })
435
+
436
+ const isTelemetryLog = Schema.is(TelemetryLog)
437
+
438
+ /**
439
+ * Annotations are a container in the durable schema, so a snapshot that spent
440
+ * the budget names the loss inside a record instead of replacing the record
441
+ * with a scalar the schema refuses.
442
+ */
443
+ const snapshotAnnotations = (value: unknown, budget: SnapshotBudget): Readonly<Record<string, unknown>> => {
444
+ const snapshot = snapshotValue(value, budget, new WeakSet())
445
+ return typeof snapshot === "object" && snapshot !== null && !Array.isArray(snapshot)
446
+ ? snapshot as Readonly<Record<string, unknown>>
447
+ : { [truncatedMarker]: truncatedMarker }
448
+ }
449
+
450
+ const snapshotLog = (options: Logger.Options<unknown>): TelemetryLog => {
451
+ const budget: SnapshotBudget = {
452
+ bytes: maximumSnapshotBytes,
453
+ members: maximumSnapshotMembers
454
+ }
455
+ try {
456
+ const span = Option.getOrUndefined(Context.getOption(options.fiber.context, Tracer.ParentSpan))
457
+ const candidate: TelemetryLog = {
458
+ version: 1,
459
+ level: options.logLevel,
460
+ message: snapshotValue(options.message, budget, new WeakSet()),
461
+ annotations: snapshotAnnotations(options.fiber.getRef(References.CurrentLogAnnotations), budget),
462
+ cause: snapshotCause(options.cause, budget),
463
+ fiberId: options.fiber.id,
464
+ ...(span === undefined ? {} : { traceId: span.traceId, spanId: span.spanId }),
465
+ timestamp: options.date.toISOString()
466
+ }
467
+ const redacted = Redaction.redact(candidate, { onTooDeep: "name" }) as TelemetryLog
468
+ // The durable row is only useful if the exported schema decodes it, so an
469
+ // oversized or schema-invalid projection degrades to the total record
470
+ // rather than to a payload every consumer throws on.
471
+ return encoder.encode(JSON.stringify(redacted)).byteLength <= maximumSnapshotBytes && isTelemetryLog(redacted)
472
+ ? redacted
473
+ : { ...candidate, message: truncatedMarker, annotations: {}, cause: { version: 1, reasons: [] } }
474
+ } catch {
475
+ return {
476
+ version: 1,
477
+ level: options.logLevel,
478
+ message: unrenderableMarker,
479
+ annotations: {},
480
+ cause: { version: 1, reasons: [] },
481
+ fiberId: options.fiber.id,
482
+ timestamp: options.date.toISOString()
483
+ }
484
+ }
485
+ }
486
+
487
+ const makeLog = (options: Logger.Options<unknown>, runId: JournalEvent.RunId): JournalEvent.Input =>
488
+ new JournalEvent.Input({
489
+ runId,
490
+ sourceId,
491
+ // The journal owns the durable per-run/source allocation. A local counter
492
+ // starts over when a layer is rebuilt and makes distinct records collide.
493
+ eventType: "telemetry.log",
494
+ payload: snapshotLog(options),
495
+ meta: {
496
+ version: 1,
497
+ source: "/observability"
498
+ }
499
+ })
500
+
501
+ /**
502
+ * Installs a bounded, drop-on-overflow logger forwarder for one explicit run.
503
+ *
504
+ * Configuration is decoded before a worker starts. The callback reads the
505
+ * queue's remaining room first, snapshots and redacts a bounded DTO
506
+ * synchronously only when a slot is free, then performs only non-blocking
507
+ * queue admission. A record a saturated queue cannot take is counted and
508
+ * discarded without being snapshotted. Queue overflow, journal `Dropped` receipts, delivery failures,
509
+ * and journal defects each advance `Metric.droppedLogRecords`. The two failure
510
+ * paths also warn through the ambient loggers with a fixed code and run id,
511
+ * and the worker keeps draining. `Accepted` and `Duplicate` do not advance the
512
+ * counter. Admission receipts cannot identify later drop-oldest evictions of
513
+ * these records or report asynchronous persistence failures or shutdown losses.
514
+ * Closing the scope interrupts the worker and may drop records still queued
515
+ * behind an in-flight write without advancing the counter.
516
+ *
517
+ * The layer replaces the ambient logger set unless `mergeWithExisting` is
518
+ * `true`, and provides `MinimumLogLevel` only when `minimumLogLevel` is given,
519
+ * so an application that set that reference elsewhere keeps it.
520
+ *
521
+ * @category layers
522
+ * @since 1.0.0-rc.0
523
+ */
524
+ export const layerJournalForwarding = (
525
+ options: Options
526
+ ): Layer.Layer<never, InvalidJournalLoggerOptions, Journal.Journal> =>
527
+ Layer.unwrap(
528
+ Effect.map(validateOptions(options), (configured) =>
529
+ Layer.merge(
530
+ Layer.effect(
531
+ Logger.CurrentLoggers,
532
+ Effect.gen(function*() {
533
+ const journal = yield* Journal.Journal
534
+ const queue = yield* Queue.bounded<JournalEvent.Input>(configured.capacity ?? 256)
535
+ const dropped = EffectMetric.update(droppedLogRecords, 1)
536
+ // This worker is forked before the logger it feeds is installed, so
537
+ // its ambient logger set cannot contain that logger and a warning
538
+ // here cannot enqueue itself. Delivery stays lossy; the loss is
539
+ // reported and counted instead of vanishing. Warning annotations
540
+ // use fixed codes so ambient sinks never receive raw failures.
541
+ const forward = Effect.fn("JournalLogger.forward")((input: JournalEvent.Input) =>
542
+ journal.emitLossy(input).pipe(
543
+ Effect.tap((receipt) => receipt._tag === "Dropped" ? dropped : Effect.void),
544
+ Effect.catch(() =>
545
+ dropped.pipe(
546
+ Effect.andThen(
547
+ Effect.annotateLogs(Effect.logWarning("A telemetry log record could not be forwarded"), {
548
+ runId: configured.runId,
549
+ code: "journal_forwarding_failed"
550
+ })
551
+ )
552
+ )
553
+ )
554
+ )
555
+ )
556
+ yield* Effect.forever(
557
+ Queue.take(queue).pipe(
558
+ Effect.flatMap(forward),
559
+ // A defect from a journal implementation would otherwise end
560
+ // this fiber for the rest of the run, silently, while the layer
561
+ // still looks installed. Interruption stays fatal.
562
+ Effect.catchCause((cause) =>
563
+ Cause.hasInterruptsOnly(cause)
564
+ ? Effect.failCause(cause)
565
+ : dropped.pipe(
566
+ Effect.andThen(
567
+ Effect.annotateLogs(
568
+ Effect.logWarning("The telemetry forwarding worker recovered from a defect"),
569
+ { runId: configured.runId, code: "journal_forwarding_defect" }
570
+ )
571
+ )
572
+ )
573
+ )
574
+ )
575
+ ).pipe(Effect.forkScoped)
576
+
577
+ const logger = Logger.make<unknown, void>((logOptions) => {
578
+ // Snapshotting and redacting the record is the expensive half of
579
+ // admission, and saturation is exactly when that work is thrown
580
+ // away, so a queue with no room refuses before the record is
581
+ // built. Nothing else runs between this read and the offer, so
582
+ // the check cannot refuse a record the queue would have taken,
583
+ // and the offer still reports a queue that closed underneath it.
584
+ const accepted = !Queue.isFullUnsafe(queue)
585
+ && Queue.offerUnsafe(queue, makeLog(logOptions, configured.runId))
586
+ if (!accepted) droppedLogRecords.updateUnsafe(1, logOptions.fiber.context)
587
+ })
588
+ const current = yield* Effect.withFiber((fiber) => Effect.sync(() => fiber.getRef(Logger.CurrentLoggers)))
589
+ return new Set(configured.mergeWithExisting === true ? current : []).add(logger)
590
+ })
591
+ ),
592
+ configured.minimumLogLevel === undefined
593
+ ? Layer.empty
594
+ : Layer.succeed(References.MinimumLogLevel, configured.minimumLogLevel)
595
+ ))
596
+ )
package/src/Logger.ts ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Effect logger layers used by flows.
3
+ *
4
+ * @since 0.1.0
5
+ */
6
+
7
+ import * as Layer from "effect/Layer"
8
+ import * as Logger from "effect/Logger"
9
+ import type * as LogLevel from "effect/LogLevel"
10
+ import * as References from "effect/References"
11
+
12
+ /**
13
+ * Options shared by the built-in logger layers.
14
+ *
15
+ * @category models
16
+ * @since 0.1.0
17
+ */
18
+ export interface Options {
19
+ /**
20
+ * Pins `References.MinimumLogLevel` for the whole application. Omitted, the
21
+ * layer leaves that reference alone and Effect's own `Info` default applies.
22
+ */
23
+ readonly minimumLogLevel?: LogLevel.LogLevel | undefined
24
+ /**
25
+ * Keeps the ambient loggers alongside this one. Defaults to `false`, which
26
+ * replaces the ambient logger set, matching Effect's own `Logger.layer`.
27
+ */
28
+ readonly mergeWithExisting?: boolean | undefined
29
+ }
30
+
31
+ /**
32
+ * Installs the minimum-level reference only when the caller named a level, so
33
+ * a layer that adds a sink does not pin the level an application chose
34
+ * elsewhere. Effect's own default for the reference is already `Info`.
35
+ */
36
+ const withMinimumLogLevel = (
37
+ layer: Layer.Layer<never>,
38
+ minimumLogLevel: LogLevel.LogLevel | undefined
39
+ ): Layer.Layer<never> =>
40
+ minimumLogLevel === undefined
41
+ ? layer
42
+ : Layer.merge(layer, Layer.succeed(References.MinimumLogLevel, minimumLogLevel))
43
+
44
+ const optionsWithDefaults = (options?: Options): {
45
+ readonly minimumLogLevel: LogLevel.LogLevel | undefined
46
+ readonly mergeWithExisting: boolean
47
+ } => ({
48
+ minimumLogLevel: options?.minimumLogLevel,
49
+ mergeWithExisting: options?.mergeWithExisting ?? false
50
+ })
51
+
52
+ /**
53
+ * Provides a human-readable development logger.
54
+ *
55
+ * @category layers
56
+ * @since 0.1.0
57
+ */
58
+ export const layerPrettyDev = (options?: Options): Layer.Layer<never> => {
59
+ const resolved = optionsWithDefaults(options)
60
+ return withMinimumLogLevel(
61
+ Logger.layer([Logger.consolePretty({ colors: "auto", mode: "auto" })], {
62
+ mergeWithExisting: resolved.mergeWithExisting
63
+ }),
64
+ resolved.minimumLogLevel
65
+ )
66
+ }
67
+
68
+ /**
69
+ * Provides one structured JSON object per log line.
70
+ *
71
+ * @category layers
72
+ * @since 0.1.0
73
+ */
74
+ export const layerStructuredJson = (options?: Options): Layer.Layer<never> => {
75
+ const resolved = optionsWithDefaults(options)
76
+ return withMinimumLogLevel(
77
+ Logger.layer([Logger.consoleJson], { mergeWithExisting: resolved.mergeWithExisting }),
78
+ resolved.minimumLogLevel
79
+ )
80
+ }
81
+
82
+ /**
83
+ * Removes the active logger set while retaining Effect's minimum-level filter.
84
+ *
85
+ * @category layers
86
+ * @since 0.1.0
87
+ */
88
+ export const layerNoop = (options?: Pick<Options, "minimumLogLevel">): Layer.Layer<never> =>
89
+ withMinimumLogLevel(
90
+ Logger.layer([]),
91
+ options?.minimumLogLevel ?? "None"
92
+ )
93
+
94
+ /**
95
+ * A logger layer that can be composed without requiring a runtime service.
96
+ *
97
+ * @category layers
98
+ * @since 0.1.0
99
+ */
100
+ export const layer = (logger: Logger.Logger<unknown, unknown>, options?: Options): Layer.Layer<never> => {
101
+ const resolved = optionsWithDefaults(options)
102
+ return withMinimumLogLevel(
103
+ Logger.layer([logger], { mergeWithExisting: resolved.mergeWithExisting }),
104
+ resolved.minimumLogLevel
105
+ )
106
+ }