chainlesschain 0.162.185 → 0.162.187

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 (184) hide show
  1. package/bin/chainlesschain.js +21 -5
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/assets/{AIOps-Co4ipAKx.js → AIOps-CETaO3s8.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-BpsoDrDq.js → ActionButton-CAe7E9TW.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-DMUqeBik.js → Analytics-BGr2mzwO.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-C6fD_dXr.js → AppLayout-BEoY-NcT.js} +5 -5
  7. package/src/assets/web-panel/assets/{Artifacts-DW0YlQlV.js → Artifacts-DUZivCgt.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-DXtHiiFK.js → Audit-C3yBs68k.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-BgmF62U-.js → BackgroundAgents-C9KwiY3O.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-D8vhVLQC.js → Backup-BoSWD_C8.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-D7nCtY2X.js → BaseInput-Dp09GkxL.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-CBZLEV2G.js → Chat-5t9d-Ja0.js} +4 -4
  13. package/src/assets/web-panel/assets/ChatBubbleRenderer-DmVku4GH.js +1 -0
  14. package/src/assets/web-panel/assets/{Checkbox-DJLkeD4M.js → Checkbox-8QJFlkqI.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-Cs6xO8Fd.js → Codegen-BJVFQ3lL.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-Dtx9wxfT.js → Col-6hdLD0oB.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-DiigJg4n.js → Community-B8opo0uY.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-B67oMDDL.js → Compact-Dv8HudiN.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-BBDl-Jos.js → Compliance-DhxuCS5N.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-Ba98hSsa.js → Cowork-Dggy6VIL.js} +3 -3
  21. package/src/assets/web-panel/assets/{Cron-DBWQZIz7.js → Cron-DSmDqJTT.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-OcrKzxAt.js → Crosschain-7bZq0j5f.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-mW2eSbjM.js → DID-BWY4kh-H.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-BmsDPmKJ.js → Dashboard-bOzPZxlu.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-CVzmCyRx.js → Dropdown-BTFJHqQu.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-vVgqKzhh.js → EmailListRenderer-BC3MOdtT.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CWY5t_ZJ.js → FamilyGuardDashboard-CFHAdTTY.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-ZGomhjxr.js → Federation-DZ_4aF_n.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-DyaknbLr.js → FormItemContext-BZje6UTj.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-CmIvN1qr.js → GenericCardRenderer-D7QfCvjN.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-DQOJIYkG.js → Git-DuG7qANG.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-CMiY-EEp.js → Governance-BDmYAgTh.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-D6IXuucf.js → Inference-DSXAfc1Q.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-6jkTL0hV.js → KnowledgeGraph-DusleniP.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-N0jD-349.js → Logs-CpImmA8p.js} +2 -2
  36. package/src/assets/web-panel/assets/MarkdownRenderer-BZ6LJBkk.js +1 -0
  37. package/src/assets/web-panel/assets/{Marketplace-WhYuhBxm.js → Marketplace-DXS8CXkE.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-DTeO28xy.js → McpTools-D151UfUI.js} +5 -5
  39. package/src/assets/web-panel/assets/{Memory-MSHzPSE-.js → Memory-DKDxnoA1.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-Cyqs3J3W.js → MobileBridge-BFZ95iKM.js} +1 -1
  41. package/src/assets/web-panel/assets/MobileProjects-CDAR-grF.js +1 -0
  42. package/src/assets/web-panel/assets/{Mtc-MWBnezNM.js → Mtc-OGJ95xJs.js} +5 -5
  43. package/src/assets/web-panel/assets/{MtcAudit-DQu35Nu-.js → MtcAudit-BmGTo1XC.js} +2 -2
  44. package/src/assets/web-panel/assets/{Multisig-CQS_M-DP.js → Multisig-XumJRqOa.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-WD16vQvY.js → NLProgramming-DHhhRZed.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-DqsiSwOq.js → Notes-B9rGxNNr.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-D3LoxO-P.js → NotificationSettings-DClLbMYV.js} +1 -1
  48. package/src/assets/web-panel/assets/{OrderTableRenderer-C5jPoTd6.js → OrderTableRenderer-I8OohT7N.js} +1 -1
  49. package/src/assets/web-panel/assets/{Organization-48aMBJso.js → Organization-M6WzaYVa.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-j-a2XkhR.js → Overflow-Cb3B5KtM.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-BYpBNYnh.js → P2P-BGk57kya.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-CYCsrrLg.js → PdhVaultBrowser-CXcKyPqK.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-ZRXGXDFb.js → Permissions-hsfJ-Y0e.js} +4 -4
  54. package/src/assets/web-panel/assets/{PersonalDataHub-6OWUPHyL.js → PersonalDataHub-BSBii4js.js} +5 -5
  55. package/src/assets/web-panel/assets/{Pipeline-FcjlDK9E.js → Pipeline-BNw52rey.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-vD_6Tldp.js → Privacy-DUpfue4E.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-B_CHOLXp.js → ProjectInit-C68NsP3h.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-rEiqQ2da.js → ProjectSettings-BJ2It9v4.js} +2 -2
  59. package/src/assets/web-panel/assets/Projects-D2FHLUa-.js +1 -0
  60. package/src/assets/web-panel/assets/{Providers-Dgtt97B4.js → Providers-EnU4VWwS.js} +1 -1
  61. package/src/assets/web-panel/assets/{QrScannerModal-DaGVylZO.js → QrScannerModal-BRt5-B_I.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-DP2_2x3u.js → QuickAsk-Jd5L9UL2.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-D29yj4ki.js → Recommend-BNDhoVIu.js} +1 -1
  64. package/src/assets/web-panel/assets/{RemoteSession-Ue2wnBLp.js → RemoteSession-BII41Mel.js} +2 -2
  65. package/src/assets/web-panel/assets/{Reputation-CrxIHqeC.js → Reputation-D1wQPUNv.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-BgSzJPCn.js → Row-DnjRegul.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-AwhYOrP9.js → RssFeed-C_T4I-nF.js} +2 -2
  68. package/src/assets/web-panel/assets/{Search-D2shpEcm.js → Search-C4lrxlLh.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-BtKzhTbT.js → Security-ByYT-VzP.js} +4 -4
  70. package/src/assets/web-panel/assets/{Services-BY0DsUSm.js → Services-BHpyEqB5.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-B3Sb0UI9.js → Skeleton-C0I23vG-.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-Ne5w0bDs.js → Skills-DGZXz22K.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-C8VkaP4T.js → Sla-DL_urP9A.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-BQdIEYP9.js → SpeechSettings-6x-hUNQ_.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-CwFafrUg.js → SyncSettings-Crk5TCXq.js} +2 -2
  76. package/src/assets/web-panel/assets/{Tasks-yJuKoDlR.js → Tasks-rZGq5PZs.js} +1 -1
  77. package/src/assets/web-panel/assets/{Templates-DYn9qCmx.js → Templates-DT5el8Q8.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-DkKReHvn.js → Tenant-mlgeyMK8.js} +1 -1
  79. package/src/assets/web-panel/assets/{Terminal-r6bd-owt.js → Terminal-C8HEA-lM.js} +2 -2
  80. package/src/assets/web-panel/assets/TimelineRenderer-DvlqBayB.js +1 -0
  81. package/src/assets/web-panel/assets/{Tokens-BnMw7MRi.js → Tokens-C7lDcNmT.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger-CTzKp4o9.js → Trigger-CM8nCnIJ.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-BBrSZm9U.js → Trust-BQDLUQ9x.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-D9fDvEMq.js → UkeySign-CVqM0OFE.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-DUi5ve-0.js → VideoEditing-BvAcQpzT.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-C8Fi1P8_.js → Wallet-zFVFaAZ2.js} +4 -4
  87. package/src/assets/web-panel/assets/{WebAuthn-s1LppYi2.js → WebAuthn-Cc522ums.js} +5 -5
  88. package/src/assets/web-panel/assets/{WorkflowEditor-DJL2C4lj.js → WorkflowEditor-Bg_wxpDo.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-D3F2ndOB.js → chat-jg0QwC8K.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-B7739-e3.js → colors-ALzYz1gO.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-CcJbFTF4.js → compact-item-DXHYUoqi.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-CuP_DEFS.js → createContext-BsGsBM5m.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-Dy1GJA4S.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-6YEXfIik.js → hasIn-BccMLzxA.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-fWgUQZ9d.js → index-08diKcTm.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-CxradkS6.js → index-2VstcImq.js} +19 -19
  97. package/src/assets/web-panel/assets/index-8zbMbbuf.js +1 -0
  98. package/src/assets/web-panel/assets/{index-DDRlBaDt.js → index-BBbbr1YB.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-Dox44j29.js → index-BTTeAiOP.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BYYSLEjn.js → index-BcJzphrP.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CRuoRapN.js → index-BdCmJ_bG.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BxSqVMGh.js → index-BfF0OGKU.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-D6Ij_2uw.js → index-BfV8RDlg.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-Dsy7_x4s.js → index-BvsII10i.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BsdSnHQE.js → index-C0AbXsDy.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-mfWQRSLn.js → index-C3Rf8wIM.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BIID4XcP.js → index-C3k3ZwkQ.js} +1 -1
  108. package/src/assets/web-panel/assets/index-C97f71oi.js +1 -0
  109. package/src/assets/web-panel/assets/{index-4h3lpWJ3.js → index-CBVIFX-x.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DHvxXXNA.js → index-CJg6Wsjc.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DjI9MkId.js → index-CLzPzN75.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-D0zdPyTD.js → index-CQ1ORdp-.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-D0K_rFC1.js → index-CSFESurl.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-b9cuvFEv.js → index-CWLWBgfe.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Nz-MUHD1.js → index-CahDve8M.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DXdnfDVK.js → index-Cg9cOVe9.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-Bh4hcNWW.js → index-Cmg_Npx5.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-Bru_kpZl.js → index-CrZHUsOZ.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-yOZYSNmG.js → index-Cz_s25m3.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DlSvpNMf.js → index-D9oV2iRR.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-LfRlSmqe.js → index-DgjQ-kdT.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BOHUo3aM.js → index-Dmk7dD-T.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-D3haorUt.js → index-Dxqiu9-F.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DV11jpeO.js → index-JS2LXdd0.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-CY0zd_jc.js → index-TeCof1s3.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-DpEKmjoa.js → index-UQXedWQz.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-C3pctdCb.js → index-koT8Q86Z.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-D_75jYWk.js → index-mIQUoUx1.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-Gxw09e9R.js → index-oWltafBn.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BhIKnQAW.js → index-rIy96PeM.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-C3ZA_h1j.js → index-rcwgNMc0.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-DGsE7MQz.js → index-srqW5_-X.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-eR6H6NR7.js → index-uzykuL_R.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-CJ1Bq2L3.js → initDefaultProps-xafkqrW3.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-rYJN_vzf.js → motion-BrtyQXLk.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-DnDK2gTP.js → move-BunUyg59.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-CKnXy7ES.js → mtc-parser-BPwjBeuE.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-3nRc3hWT.js → omit-BpyqZlNO.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-Ddjj3sLu.js → pickAttrs-DDrV0XjE.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-CbsbphQY.js → placementArrow-1A18Pcl2.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-D9aZ4VDJ.js → responsiveObserve-B8Fnlmso.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-D4pvdFj0.js → slide-x-Fwb2Zm.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-C2W-vY-S.js → statusUtils-aAG5yJl_.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-Cbq5150d.js → styleChecker-BRQSTMRL.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-BA2B7R82.js → useFlexGapSupport-D5X_KozY.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-CLrwg3xg.js → useFs-DN_YIjP-.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-DQeAI5qs.js → usePersonalDataHub-BizxAvjw.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-8BDGy-YU.js → vnode-B5oAxN93.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-B2SzFprl.js → zoom-DJhvyzKv.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/commands/eval.js +7 -0
  152. package/src/commands/status.js +55 -0
  153. package/src/commands/team.js +1805 -448
  154. package/src/data/changelog.json +26 -0
  155. package/src/harness/worktree-isolator.js +671 -79
  156. package/src/lazy-dispatch.js +12 -1
  157. package/src/lib/agent-team/task-lease.js +378 -78
  158. package/src/lib/agent-team/team-agent-stream.js +488 -0
  159. package/src/lib/agent-team/team-budget.js +216 -20
  160. package/src/lib/agent-team/team-mailbox.js +389 -15
  161. package/src/lib/agent-team/team-run-state-lock.js +209 -0
  162. package/src/lib/agent-team/team-runner.js +547 -63
  163. package/src/lib/agent-team/team-scope-lock.js +409 -0
  164. package/src/lib/agent-team/team-task-contract.js +277 -0
  165. package/src/lib/agent-team/team-worktree.js +1171 -62
  166. package/src/lib/agent-worktree.js +19 -20
  167. package/src/lib/collaboration-run-store.js +983 -68
  168. package/src/lib/observability/index.js +191 -19
  169. package/src/lib/observability/otlp-exporter.js +1209 -207
  170. package/src/lib/otlp-exporter.js +8 -36
  171. package/src/lib/with-file-lock.js +190 -24
  172. package/src/lib/worktree-isolator.js +3 -0
  173. package/src/lib/worktree-sparse.js +23 -0
  174. package/src/program-base.js +9 -2
  175. package/src/runtime/headless-runner.js +45 -21
  176. package/src/runtime/headless-stream.js +6 -1
  177. package/src/assets/web-panel/assets/ChatBubbleRenderer-CtDb-2Qu.js +0 -1
  178. package/src/assets/web-panel/assets/MarkdownRenderer-BL1IVXm-.js +0 -1
  179. package/src/assets/web-panel/assets/MobileProjects-7IN2hWHg.js +0 -1
  180. package/src/assets/web-panel/assets/Projects-Bg8obRAM.js +0 -1
  181. package/src/assets/web-panel/assets/TimelineRenderer-Mq2juUC3.js +0 -1
  182. package/src/assets/web-panel/assets/devWarning-Cj_36GHq.js +0 -1
  183. package/src/assets/web-panel/assets/index-CezFrAmD.js +0 -1
  184. package/src/assets/web-panel/assets/index-Cxgguez6.js +0 -1
@@ -1,260 +1,1262 @@
1
1
  /**
2
- * OTLP Exporter — M8 Observability
3
- * OpenTelemetry Protocol exporter for spans and metrics
4
- * Sends to localhost:4318 by default, with batching and retry
2
+ * Dependency-free OTLP/HTTP JSON exporter.
3
+ *
4
+ * The OpenTelemetry Collector accepts the proto3 JSON mapping at
5
+ * `/v1/traces` and `/v1/metrics`. This exporter deliberately stays outside the
6
+ * execution/audit layers: telemetry failure must never change command results.
7
+ *
8
+ * Supported OpenTelemetry environment variables:
9
+ * OTEL_SDK_DISABLED
10
+ * OTEL_SERVICE_NAME
11
+ * OTEL_RESOURCE_ATTRIBUTES
12
+ * OTEL_EXPORTER_OTLP_{,TRACES_,METRICS_}ENDPOINT
13
+ * OTEL_EXPORTER_OTLP_{,TRACES_,METRICS_}PROTOCOL
14
+ * OTEL_EXPORTER_OTLP_{,TRACES_,METRICS_}HEADERS
15
+ * OTEL_EXPORTER_OTLP_{,TRACES_,METRICS_}TIMEOUT
16
+ * OTEL_EXPORTER_OTLP_{,TRACES_,METRICS_}COMPRESSION
17
+ * OTEL_EXPORTER_OTLP_{,TRACES_,METRICS_}CERTIFICATE
18
+ * OTEL_EXPORTER_OTLP_{,TRACES_,METRICS_}CLIENT_CERTIFICATE
19
+ * OTEL_EXPORTER_OTLP_{,TRACES_,METRICS_}CLIENT_KEY
20
+ *
21
+ * ChainlessChain-only reliability controls:
22
+ * CC_OTEL_MAX_QUEUE_SIZE, CC_OTEL_MAX_EXPORT_BATCH_SIZE,
23
+ * CC_OTEL_MAX_ATTEMPTS, CC_OTEL_SPOOL_DIR.
5
24
  */
6
25
 
26
+ import fs from "node:fs";
7
27
  import http from "node:http";
28
+ import http2 from "node:http2";
8
29
  import https from "node:https";
30
+ import path from "node:path";
31
+ import { createHash, randomUUID } from "node:crypto";
32
+ import { gzipSync } from "node:zlib";
9
33
  import { URL } from "node:url";
34
+ import { getHomeDir } from "../paths.js";
35
+ import { redactSecrets } from "../secret-scan.js";
10
36
 
11
- class OtlpExporter {
12
- constructor(options = {}) {
13
- this.endpoint =
14
- options.endpoint ||
15
- process.env.OTEL_EXPORTER_OTLP_ENDPOINT ||
16
- "http://localhost:4318";
17
- this.headers = options.headers || {};
18
- this.serviceName =
19
- options.serviceName ||
20
- process.env.OTEL_SERVICE_NAME ||
21
- "chainlesschain-cli";
22
- this.batchSize = options.batchSize || 512;
23
- this.flushIntervalMs = options.flushIntervalMs || 5000;
24
- this._spanBuffer = [];
25
- this._metricBuffer = [];
37
+ const SIGNALS = new Set(["traces", "metrics"]);
38
+ const RETRYABLE_STATUS = new Set([408, 429, 502, 503, 504]);
39
+ const DEFAULT_TIMEOUT_MS = 10_000;
40
+ const DEFAULT_SCHEDULE_DELAY_MS = 5_000;
41
+ const DEFAULT_MAX_QUEUE_SIZE = 2_048;
42
+ const DEFAULT_MAX_EXPORT_BATCH_SIZE = 128;
43
+ const DEFAULT_MAX_ATTEMPTS = 5;
44
+ const MAX_BACKOFF_MS = 30_000;
45
+ const MAX_RECOVERY_FILE_AGE_MS = 24 * 60 * 60 * 1_000;
46
+
47
+ function positiveInt(value, fallback, { min = 1, max = 1_000_000 } = {}) {
48
+ const parsed = Number(value);
49
+ if (!Number.isFinite(parsed)) return fallback;
50
+ const rounded = Math.floor(parsed);
51
+ return rounded >= min && rounded <= max ? rounded : fallback;
52
+ }
53
+
54
+ function envValue(env, signal, suffix) {
55
+ const scoped = env[`OTEL_EXPORTER_OTLP_${signal.toUpperCase()}_${suffix}`];
56
+ return scoped == null || scoped === ""
57
+ ? env[`OTEL_EXPORTER_OTLP_${suffix}`]
58
+ : scoped;
59
+ }
60
+
61
+ function appendSignalPath(endpoint, signal) {
62
+ const url = new URL(endpoint);
63
+ const suffix = `/v1/${signal}`;
64
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}${suffix}`;
65
+ return url.toString();
66
+ }
67
+
68
+ function normalizeProtocol(value) {
69
+ const protocol = String(value || "http/json").toLowerCase();
70
+ if (["http/json", "http/protobuf", "grpc"].includes(protocol)) {
71
+ return { protocol, error: null };
72
+ }
73
+ return {
74
+ protocol: null,
75
+ error: `unsupported OTLP protocol: ${protocol}`,
76
+ };
77
+ }
78
+
79
+ function resolveSignalEndpoint(options, env, signal, protocol) {
80
+ const explicit = options[`${signal}Endpoint`];
81
+ if (explicit) return new URL(explicit).toString();
82
+ const scoped = envValue(env, signal, "ENDPOINT");
83
+ const general =
84
+ options.endpoint ||
85
+ env.OTEL_EXPORTER_OTLP_ENDPOINT ||
86
+ (options.enabled === true ? "http://localhost:4318" : null);
87
+ if (!scoped && !general) return null;
88
+ return scoped
89
+ ? new URL(scoped).toString()
90
+ : protocol === "grpc"
91
+ ? new URL(general).toString()
92
+ : appendSignalPath(general, signal);
93
+ }
94
+
95
+ export function parseOtlpHeaders(value) {
96
+ if (!value) return {};
97
+ if (typeof value === "object" && !Array.isArray(value)) return { ...value };
98
+ const headers = {};
99
+ for (const pair of String(value).split(",")) {
100
+ const separator = pair.indexOf("=");
101
+ if (separator <= 0) continue;
102
+ try {
103
+ const key = decodeURIComponent(pair.slice(0, separator).trim());
104
+ const headerValue = decodeURIComponent(pair.slice(separator + 1).trim());
105
+ if (key) headers[key] = headerValue;
106
+ } catch {
107
+ // A malformed percent escape invalidates only that header.
108
+ }
109
+ }
110
+ return headers;
111
+ }
112
+
113
+ export function parseResourceAttributes(value) {
114
+ return parseOtlpHeaders(value);
115
+ }
116
+
117
+ function readTlsFile(filePath, readFileSync = fs.readFileSync) {
118
+ if (!filePath) return undefined;
119
+ return readFileSync(filePath);
120
+ }
121
+
122
+ function signalTlsOptions(options, env, signal, readFileSync) {
123
+ const from = (suffix, optionKey) =>
124
+ options[`${signal}${optionKey}`] ||
125
+ options[optionKey[0].toLowerCase() + optionKey.slice(1)] ||
126
+ envValue(env, signal, suffix);
127
+ return {
128
+ ca: readTlsFile(from("CERTIFICATE", "Certificate"), readFileSync),
129
+ cert: readTlsFile(
130
+ from("CLIENT_CERTIFICATE", "ClientCertificate"),
131
+ readFileSync,
132
+ ),
133
+ key: readTlsFile(from("CLIENT_KEY", "ClientKey"), readFileSync),
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Resolve standard OTel configuration without mutating `process.env`.
139
+ * A Collector is opt-in: an endpoint (flag/env) or `enabled:true` is required.
140
+ */
141
+ export function resolveOtlpConfig(options = {}, env = process.env, deps = {}) {
142
+ const disabled =
143
+ options.enabled === false ||
144
+ String(env.OTEL_SDK_DISABLED || "").toLowerCase() === "true";
145
+ const traceProtocol = normalizeProtocol(
146
+ options.tracesProtocol || envValue(env, "traces", "PROTOCOL"),
147
+ );
148
+ const metricProtocol = normalizeProtocol(
149
+ options.metricsProtocol || envValue(env, "metrics", "PROTOCOL"),
150
+ );
151
+ const tracesEndpoint =
152
+ disabled || !traceProtocol.protocol
153
+ ? null
154
+ : resolveSignalEndpoint(options, env, "traces", traceProtocol.protocol);
155
+ const metricsEndpoint =
156
+ disabled || !metricProtocol.protocol
157
+ ? null
158
+ : resolveSignalEndpoint(options, env, "metrics", metricProtocol.protocol);
159
+ const enabled = !disabled && Boolean(tracesEndpoint || metricsEndpoint);
160
+ const readFileSync = deps.readFileSync || fs.readFileSync;
161
+ const commonHeaders = parseOtlpHeaders(
162
+ options.headers || env.OTEL_EXPORTER_OTLP_HEADERS,
163
+ );
164
+ const signal = (name, endpoint, protocolConfig) => ({
165
+ endpoint,
166
+ protocol: protocolConfig.protocol,
167
+ configError: protocolConfig.error,
168
+ headers: {
169
+ ...commonHeaders,
170
+ ...parseOtlpHeaders(
171
+ options[`${name}Headers`] ||
172
+ env[`OTEL_EXPORTER_OTLP_${name.toUpperCase()}_HEADERS`],
173
+ ),
174
+ },
175
+ timeoutMs: positiveInt(
176
+ options[`${name}TimeoutMs`] || envValue(env, name, "TIMEOUT"),
177
+ DEFAULT_TIMEOUT_MS,
178
+ { max: 300_000 },
179
+ ),
180
+ compression: String(
181
+ options[`${name}Compression`] ||
182
+ envValue(env, name, "COMPRESSION") ||
183
+ "none",
184
+ ).toLowerCase(),
185
+ tls: endpoint
186
+ ? signalTlsOptions(options, env, name, readFileSync)
187
+ : { ca: undefined, cert: undefined, key: undefined },
188
+ });
189
+ const serviceName =
190
+ options.serviceName || env.OTEL_SERVICE_NAME || "chainlesschain-cli";
191
+ const resourceAttributes = {
192
+ ...parseResourceAttributes(env.OTEL_RESOURCE_ATTRIBUTES),
193
+ ...(options.resourceAttributes || {}),
194
+ "service.name": serviceName,
195
+ };
196
+ return {
197
+ enabled,
198
+ serviceName,
199
+ resourceAttributes,
200
+ traces: signal("traces", tracesEndpoint, traceProtocol),
201
+ metrics: signal("metrics", metricsEndpoint, metricProtocol),
202
+ scheduleDelayMs: positiveInt(
203
+ options.scheduleDelayMs || env.OTEL_BSP_SCHEDULE_DELAY,
204
+ DEFAULT_SCHEDULE_DELAY_MS,
205
+ { max: 300_000 },
206
+ ),
207
+ maxQueueSize: positiveInt(
208
+ options.maxQueueSize || env.CC_OTEL_MAX_QUEUE_SIZE,
209
+ DEFAULT_MAX_QUEUE_SIZE,
210
+ { max: 100_000 },
211
+ ),
212
+ maxExportBatchSize: positiveInt(
213
+ options.maxExportBatchSize || env.CC_OTEL_MAX_EXPORT_BATCH_SIZE,
214
+ DEFAULT_MAX_EXPORT_BATCH_SIZE,
215
+ { max: 10_000 },
216
+ ),
217
+ maxAttempts: positiveInt(
218
+ options.maxAttempts || env.CC_OTEL_MAX_ATTEMPTS,
219
+ DEFAULT_MAX_ATTEMPTS,
220
+ { max: 20 },
221
+ ),
222
+ spoolDir:
223
+ options.spoolDir ||
224
+ env.CC_OTEL_SPOOL_DIR ||
225
+ path.join(getHomeDir(), "telemetry", "otlp-queue"),
226
+ };
227
+ }
228
+
229
+ function cloneAndRedact(value, redact = true) {
230
+ if (Array.isArray(value))
231
+ return value.map((item) => cloneAndRedact(item, redact));
232
+ if (!value || typeof value !== "object") return value;
233
+ const out = {};
234
+ for (const [key, child] of Object.entries(value)) {
235
+ if (redact && key === "stringValue" && typeof child === "string") {
236
+ out[key] = redactSecrets(child);
237
+ } else {
238
+ out[key] = cloneAndRedact(child, redact);
239
+ }
240
+ }
241
+ return out;
242
+ }
243
+
244
+ function otlpValue(value, redact = true) {
245
+ if (typeof value === "boolean") return { boolValue: value };
246
+ if (typeof value === "number") {
247
+ return Number.isInteger(value)
248
+ ? { intValue: String(value) }
249
+ : { doubleValue: value };
250
+ }
251
+ return {
252
+ stringValue: redact ? redactSecrets(String(value)) : String(value),
253
+ };
254
+ }
255
+
256
+ function resourceAttributes(attributes, redact = true) {
257
+ return Object.entries(attributes).map(([key, value]) => ({
258
+ key,
259
+ value: otlpValue(value, redact),
260
+ }));
261
+ }
262
+
263
+ function protobufVarint(value) {
264
+ let remaining = BigInt.asUintN(64, BigInt(value || 0));
265
+ const bytes = [];
266
+ do {
267
+ let byte = Number(remaining & 0x7fn);
268
+ remaining >>= 7n;
269
+ if (remaining > 0n) byte |= 0x80;
270
+ bytes.push(byte);
271
+ } while (remaining > 0n);
272
+ return Buffer.from(bytes);
273
+ }
274
+
275
+ function protobufTag(field, wireType) {
276
+ return protobufVarint((field << 3) | wireType);
277
+ }
278
+
279
+ function protobufBytes(field, value) {
280
+ const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value || []);
281
+ return Buffer.concat([
282
+ protobufTag(field, 2),
283
+ protobufVarint(bytes.length),
284
+ bytes,
285
+ ]);
286
+ }
287
+
288
+ function protobufString(field, value) {
289
+ return value == null || value === ""
290
+ ? Buffer.alloc(0)
291
+ : protobufBytes(field, Buffer.from(String(value), "utf8"));
292
+ }
293
+
294
+ function protobufMessage(field, chunks) {
295
+ const body = Buffer.concat(chunks.filter((chunk) => chunk.length > 0));
296
+ return body.length > 0 ? protobufBytes(field, body) : Buffer.alloc(0);
297
+ }
298
+
299
+ function protobufUInt(field, value) {
300
+ return value == null || Number(value) === 0
301
+ ? Buffer.alloc(0)
302
+ : Buffer.concat([protobufTag(field, 0), protobufVarint(value)]);
303
+ }
304
+
305
+ function protobufFixed64(field, value) {
306
+ if (value == null || value === "") return Buffer.alloc(0);
307
+ return Buffer.concat([protobufTag(field, 1), protobufFixed64Body(value)]);
308
+ }
309
+
310
+ function protobufFixed64Body(value) {
311
+ const body = Buffer.alloc(8);
312
+ body.writeBigUInt64LE(BigInt.asUintN(64, BigInt(value)));
313
+ return body;
314
+ }
315
+
316
+ function protobufFixed32(field, value) {
317
+ if (value == null || Number(value) === 0) return Buffer.alloc(0);
318
+ const body = Buffer.alloc(4);
319
+ body.writeUInt32LE(Number(value) >>> 0);
320
+ return Buffer.concat([protobufTag(field, 5), body]);
321
+ }
322
+
323
+ function protobufDouble(field, value) {
324
+ if (value == null) return Buffer.alloc(0);
325
+ const body = Buffer.alloc(8);
326
+ body.writeDoubleLE(Number(value), 0);
327
+ return Buffer.concat([protobufTag(field, 1), body]);
328
+ }
329
+
330
+ function protobufHexBytes(field, value, expectedBytes) {
331
+ if (!value) return Buffer.alloc(0);
332
+ const normalized = String(value).replace(/[^a-fA-F0-9]/g, "");
333
+ if (normalized.length !== expectedBytes * 2) return Buffer.alloc(0);
334
+ return protobufBytes(field, Buffer.from(normalized, "hex"));
335
+ }
336
+
337
+ function encodeAnyValue(value = {}) {
338
+ if (Object.hasOwn(value, "stringValue")) {
339
+ return protobufBytes(1, Buffer.from(String(value.stringValue), "utf8"));
340
+ }
341
+ if (Object.hasOwn(value, "boolValue")) {
342
+ return Buffer.concat([
343
+ protobufTag(2, 0),
344
+ protobufVarint(value.boolValue ? 1 : 0),
345
+ ]);
346
+ }
347
+ if (Object.hasOwn(value, "intValue")) {
348
+ return Buffer.concat([protobufTag(3, 0), protobufVarint(value.intValue)]);
349
+ }
350
+ if (Object.hasOwn(value, "doubleValue")) {
351
+ return protobufDouble(4, value.doubleValue);
352
+ }
353
+ if (value.arrayValue?.values) {
354
+ return protobufMessage(
355
+ 5,
356
+ value.arrayValue.values.map((item) =>
357
+ protobufMessage(1, [encodeAnyValue(item)]),
358
+ ),
359
+ );
360
+ }
361
+ if (value.kvlistValue?.values) {
362
+ return protobufMessage(
363
+ 6,
364
+ value.kvlistValue.values.map((item) =>
365
+ protobufMessage(1, [encodeKeyValue(item)]),
366
+ ),
367
+ );
368
+ }
369
+ if (value.bytesValue) {
370
+ return protobufBytes(7, Buffer.from(value.bytesValue, "base64"));
371
+ }
372
+ return Buffer.alloc(0);
373
+ }
374
+
375
+ function encodeKeyValue(attribute = {}) {
376
+ return Buffer.concat([
377
+ protobufString(1, attribute.key),
378
+ protobufMessage(2, [encodeAnyValue(attribute.value)]),
379
+ ]);
380
+ }
381
+
382
+ function encodeAttributes(attributes = [], field = 1) {
383
+ return attributes.map((attribute) =>
384
+ protobufMessage(field, [encodeKeyValue(attribute)]),
385
+ );
386
+ }
387
+
388
+ function encodeResource(resource = {}) {
389
+ return Buffer.concat([
390
+ ...encodeAttributes(resource.attributes, 1),
391
+ protobufUInt(2, resource.droppedAttributesCount),
392
+ ]);
393
+ }
394
+
395
+ function encodeScope(scope = {}) {
396
+ return Buffer.concat([
397
+ protobufString(1, scope.name),
398
+ protobufString(2, scope.version),
399
+ ...encodeAttributes(scope.attributes, 3),
400
+ protobufUInt(4, scope.droppedAttributesCount),
401
+ ]);
402
+ }
403
+
404
+ function encodeSpanEvent(event = {}) {
405
+ return Buffer.concat([
406
+ protobufFixed64(1, event.timeUnixNano),
407
+ protobufString(2, event.name),
408
+ ...encodeAttributes(event.attributes, 3),
409
+ protobufUInt(4, event.droppedAttributesCount),
410
+ ]);
411
+ }
412
+
413
+ function encodeSpanStatus(status = {}) {
414
+ return Buffer.concat([
415
+ protobufString(2, status.message),
416
+ protobufUInt(3, status.code),
417
+ ]);
418
+ }
419
+
420
+ function encodeSpan(span = {}) {
421
+ return Buffer.concat([
422
+ protobufHexBytes(1, span.traceId, 16),
423
+ protobufHexBytes(2, span.spanId, 8),
424
+ protobufString(3, span.traceState),
425
+ protobufHexBytes(4, span.parentSpanId, 8),
426
+ protobufString(5, span.name),
427
+ protobufUInt(6, span.kind),
428
+ protobufFixed64(7, span.startTimeUnixNano),
429
+ protobufFixed64(8, span.endTimeUnixNano),
430
+ ...encodeAttributes(span.attributes, 9),
431
+ protobufUInt(10, span.droppedAttributesCount),
432
+ ...(span.events || []).map((event) =>
433
+ protobufMessage(11, [encodeSpanEvent(event)]),
434
+ ),
435
+ protobufUInt(12, span.droppedEventsCount),
436
+ protobufMessage(15, [encodeSpanStatus(span.status)]),
437
+ protobufFixed32(16, span.flags),
438
+ ]);
439
+ }
440
+
441
+ function encodeScopeSpans(scopeSpans = {}) {
442
+ return Buffer.concat([
443
+ protobufMessage(1, [encodeScope(scopeSpans.scope)]),
444
+ ...(scopeSpans.spans || []).map((span) =>
445
+ protobufMessage(2, [encodeSpan(span)]),
446
+ ),
447
+ protobufString(3, scopeSpans.schemaUrl),
448
+ ]);
449
+ }
450
+
451
+ function encodeResourceSpans(resourceSpans = {}) {
452
+ return Buffer.concat([
453
+ protobufMessage(1, [encodeResource(resourceSpans.resource)]),
454
+ ...(resourceSpans.scopeSpans || []).map((scope) =>
455
+ protobufMessage(2, [encodeScopeSpans(scope)]),
456
+ ),
457
+ protobufString(3, resourceSpans.schemaUrl),
458
+ ]);
459
+ }
460
+
461
+ function encodeNumberDataPoint(point = {}) {
462
+ const chunks = [
463
+ ...encodeAttributes(point.attributes, 7),
464
+ protobufFixed64(2, point.startTimeUnixNano),
465
+ protobufFixed64(3, point.timeUnixNano),
466
+ ];
467
+ if (Object.hasOwn(point, "asDouble")) {
468
+ chunks.push(protobufDouble(4, point.asDouble));
469
+ } else if (Object.hasOwn(point, "asInt")) {
470
+ chunks.push(protobufFixed64(6, point.asInt));
471
+ }
472
+ chunks.push(protobufUInt(8, point.flags));
473
+ return Buffer.concat(chunks);
474
+ }
475
+
476
+ function encodeGauge(gauge = {}) {
477
+ return Buffer.concat(
478
+ (gauge.dataPoints || []).map((point) =>
479
+ protobufMessage(1, [encodeNumberDataPoint(point)]),
480
+ ),
481
+ );
482
+ }
483
+
484
+ function encodeSum(sum = {}) {
485
+ return Buffer.concat([
486
+ ...(sum.dataPoints || []).map((point) =>
487
+ protobufMessage(1, [encodeNumberDataPoint(point)]),
488
+ ),
489
+ protobufUInt(2, sum.aggregationTemporality),
490
+ ...(sum.isMonotonic
491
+ ? [Buffer.concat([protobufTag(3, 0), protobufVarint(1)])]
492
+ : []),
493
+ ]);
494
+ }
495
+
496
+ function encodeHistogramPoint(point = {}) {
497
+ const bucketCounts = (point.bucketCounts || []).map((value) =>
498
+ protobufFixed64Body(value),
499
+ );
500
+ const bounds = (point.explicitBounds || []).map((value) => {
501
+ const body = Buffer.alloc(8);
502
+ body.writeDoubleLE(Number(value), 0);
503
+ return body;
504
+ });
505
+ return Buffer.concat([
506
+ ...encodeAttributes(point.attributes, 9),
507
+ protobufFixed64(2, point.startTimeUnixNano),
508
+ protobufFixed64(3, point.timeUnixNano),
509
+ protobufFixed64(4, point.count || 0),
510
+ ...(Object.hasOwn(point, "sum") ? [protobufDouble(5, point.sum)] : []),
511
+ ...(bucketCounts.length > 0
512
+ ? [protobufBytes(6, Buffer.concat(bucketCounts))]
513
+ : []),
514
+ ...(bounds.length > 0 ? [protobufBytes(7, Buffer.concat(bounds))] : []),
515
+ protobufUInt(10, point.flags),
516
+ ...(Object.hasOwn(point, "min") ? [protobufDouble(11, point.min)] : []),
517
+ ...(Object.hasOwn(point, "max") ? [protobufDouble(12, point.max)] : []),
518
+ ]);
519
+ }
520
+
521
+ function encodeHistogram(histogram = {}) {
522
+ return Buffer.concat([
523
+ ...(histogram.dataPoints || []).map((point) =>
524
+ protobufMessage(1, [encodeHistogramPoint(point)]),
525
+ ),
526
+ protobufUInt(2, histogram.aggregationTemporality),
527
+ ]);
528
+ }
529
+
530
+ function encodeMetric(metric = {}) {
531
+ return Buffer.concat([
532
+ protobufString(1, metric.name),
533
+ protobufString(2, metric.description),
534
+ protobufString(3, metric.unit),
535
+ ...(metric.gauge ? [protobufMessage(5, [encodeGauge(metric.gauge)])] : []),
536
+ ...(metric.sum ? [protobufMessage(7, [encodeSum(metric.sum)])] : []),
537
+ ...(metric.histogram
538
+ ? [protobufMessage(9, [encodeHistogram(metric.histogram)])]
539
+ : []),
540
+ ]);
541
+ }
542
+
543
+ function encodeScopeMetrics(scopeMetrics = {}) {
544
+ return Buffer.concat([
545
+ protobufMessage(1, [encodeScope(scopeMetrics.scope)]),
546
+ ...(scopeMetrics.metrics || []).map((metric) =>
547
+ protobufMessage(2, [encodeMetric(metric)]),
548
+ ),
549
+ protobufString(3, scopeMetrics.schemaUrl),
550
+ ]);
551
+ }
552
+
553
+ function encodeResourceMetrics(resourceMetrics = {}) {
554
+ return Buffer.concat([
555
+ protobufMessage(1, [encodeResource(resourceMetrics.resource)]),
556
+ ...(resourceMetrics.scopeMetrics || []).map((scope) =>
557
+ protobufMessage(2, [encodeScopeMetrics(scope)]),
558
+ ),
559
+ protobufString(3, resourceMetrics.schemaUrl),
560
+ ]);
561
+ }
562
+
563
+ /** Encode an OTLP JSON-shaped request as its canonical protobuf message. */
564
+ export function encodeOtlpProtobuf(signal, payload) {
565
+ if (signal === "traces") {
566
+ return Buffer.concat(
567
+ (payload?.resourceSpans || []).map((resource) =>
568
+ protobufMessage(1, [encodeResourceSpans(resource)]),
569
+ ),
570
+ );
571
+ }
572
+ if (signal === "metrics") {
573
+ return Buffer.concat(
574
+ (payload?.resourceMetrics || []).map((resource) =>
575
+ protobufMessage(1, [encodeResourceMetrics(resource)]),
576
+ ),
577
+ );
578
+ }
579
+ throw new RangeError(`Unsupported OTLP signal: ${signal}`);
580
+ }
581
+
582
+ function mergePayloads(signal, payloads) {
583
+ const field = signal === "traces" ? "resourceSpans" : "resourceMetrics";
584
+ return {
585
+ [field]: payloads.flatMap((payload) =>
586
+ Array.isArray(payload?.[field]) ? payload[field] : [],
587
+ ),
588
+ };
589
+ }
590
+
591
+ function retryAfterMs(headers, now = Date.now()) {
592
+ const value = headers?.["retry-after"];
593
+ if (!value) return null;
594
+ const seconds = Number(value);
595
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);
596
+ const at = Date.parse(value);
597
+ return Number.isFinite(at) ? Math.max(0, at - now) : null;
598
+ }
599
+
600
+ function safeTransportHeaders(headers) {
601
+ const reserved = new Set([
602
+ "content-length",
603
+ "content-type",
604
+ "content-encoding",
605
+ "grpc-encoding",
606
+ "te",
607
+ ]);
608
+ return Object.fromEntries(
609
+ Object.entries(headers || {})
610
+ .map(([key, value]) => [key.toLowerCase(), String(value)])
611
+ .filter(([key]) => !key.startsWith(":") && !reserved.has(key)),
612
+ );
613
+ }
614
+
615
+ function backoffMs(attempt) {
616
+ return Math.min(MAX_BACKOFF_MS, 250 * 2 ** Math.max(0, attempt - 1));
617
+ }
618
+
619
+ function endpointKey(config) {
620
+ return createHash("sha256")
621
+ .update(
622
+ `${config.traces.protocol || ""}\0${config.traces.endpoint || ""}\0${config.metrics.protocol || ""}\0${config.metrics.endpoint || ""}`,
623
+ "utf8",
624
+ )
625
+ .digest("hex")
626
+ .slice(0, 16);
627
+ }
628
+
629
+ function isPidAlive(pid) {
630
+ if (!Number.isInteger(pid) || pid <= 0) return false;
631
+ try {
632
+ process.kill(pid, 0);
633
+ return true;
634
+ } catch {
635
+ return false;
636
+ }
637
+ }
638
+
639
+ export class OtlpExporter {
640
+ constructor(options = {}, deps = {}) {
641
+ this._env = deps.env || process.env;
642
+ this.config = options.config || resolveOtlpConfig(options, this._env, deps);
643
+ this._http = deps.http || http;
644
+ this._http2 = deps.http2 || http2;
645
+ this._https = deps.https || https;
646
+ this._fs = deps.fs || fs;
647
+ this._now = deps.now || (() => Date.now());
648
+ this._setInterval = deps.setInterval || setInterval;
649
+ this._clearInterval = deps.clearInterval || clearInterval;
650
+ this._sleep =
651
+ deps.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
652
+ this._isPidAlive = deps.isPidAlive || isPidAlive;
653
+ this._pid = deps.pid || process.pid;
654
+ this._instanceId = deps.instanceId || randomUUID();
655
+ this._queue = [];
656
+ this._flushPromise = null;
26
657
  this._timer = null;
27
- this._droppedSpans = 0;
28
- this._droppedMetrics = 0;
29
- this._failedExports = 0;
30
- this._successfulExports = 0;
31
- this._enabled = options.enabled !== false;
32
- if (this._enabled) {
33
- this._startFlushTimer();
658
+ this._closed = false;
659
+ this._stats = {
660
+ enqueued: 0,
661
+ exported: 0,
662
+ retried: 0,
663
+ dropped: 0,
664
+ permanentFailures: 0,
665
+ recovered: 0,
666
+ spoolErrors: 0,
667
+ configurationErrors: [
668
+ this.config.traces.configError,
669
+ this.config.metrics.configError,
670
+ ]
671
+ .filter(Boolean)
672
+ .filter((value, index, values) => values.indexOf(value) === index),
673
+ lastError: null,
674
+ };
675
+ this._spoolKey = endpointKey(this.config);
676
+ this._spoolFile = path.join(
677
+ this.config.spoolDir,
678
+ `active-${this._spoolKey}-${this._pid}-${this._instanceId}.json`,
679
+ );
680
+ if (this.config.enabled) {
681
+ this._recoverSpool();
682
+ this._startTimer();
683
+ }
684
+ }
685
+
686
+ get enabled() {
687
+ return this.config.enabled && !this._closed;
688
+ }
689
+
690
+ _startTimer() {
691
+ this._timer = this._setInterval(
692
+ () => void this.flush(),
693
+ this.config.scheduleDelayMs,
694
+ );
695
+ this._timer?.unref?.();
696
+ }
697
+
698
+ _recoverSpool() {
699
+ try {
700
+ this._fs.mkdirSync(this.config.spoolDir, {
701
+ recursive: true,
702
+ mode: 0o700,
703
+ });
704
+ const prefix = `active-${this._spoolKey}-`;
705
+ const files = this._fs
706
+ .readdirSync(this.config.spoolDir)
707
+ .filter((name) => name.startsWith(prefix) && name.endsWith(".json"));
708
+ for (const name of files) {
709
+ const source = path.join(this.config.spoolDir, name);
710
+ if (source === this._spoolFile) continue;
711
+ const match = name.match(/^active-[^-]+-(\d+)-/);
712
+ const ownerPid = match ? Number(match[1]) : 0;
713
+ let stat;
714
+ try {
715
+ stat = this._fs.statSync(source);
716
+ } catch {
717
+ continue;
718
+ }
719
+ const recentlyOwned =
720
+ this._now() - stat.mtimeMs < MAX_RECOVERY_FILE_AGE_MS;
721
+ if (recentlyOwned && this._isPidAlive(ownerPid)) continue;
722
+ const claimed = `${source}.claim-${this._instanceId}`;
723
+ try {
724
+ this._fs.renameSync(source, claimed);
725
+ } catch {
726
+ continue;
727
+ }
728
+ try {
729
+ const parsed = JSON.parse(this._fs.readFileSync(claimed, "utf8"));
730
+ const recovered = Array.isArray(parsed?.queue) ? parsed.queue : [];
731
+ for (const item of recovered) this._push(item, { recovered: true });
732
+ } catch (error) {
733
+ this._stats.spoolErrors++;
734
+ this._stats.lastError = `spool recovery: ${error.message}`;
735
+ } finally {
736
+ try {
737
+ this._fs.unlinkSync(claimed);
738
+ } catch {
739
+ // A leftover claimed file is ignored by later recovery scans.
740
+ }
741
+ }
742
+ }
743
+ this._persist();
744
+ } catch (error) {
745
+ this._stats.spoolErrors++;
746
+ this._stats.lastError = `spool init: ${error.message}`;
747
+ }
748
+ }
749
+
750
+ _persist() {
751
+ if (!this.config.enabled) return;
752
+ try {
753
+ this._fs.mkdirSync(this.config.spoolDir, {
754
+ recursive: true,
755
+ mode: 0o700,
756
+ });
757
+ if (this._queue.length === 0) {
758
+ try {
759
+ this._fs.unlinkSync(this._spoolFile);
760
+ } catch (error) {
761
+ if (error?.code !== "ENOENT") throw error;
762
+ }
763
+ return;
764
+ }
765
+ const temporary = `${this._spoolFile}.tmp`;
766
+ this._fs.writeFileSync(
767
+ temporary,
768
+ `${JSON.stringify({
769
+ version: 1,
770
+ ownerPid: this._pid,
771
+ endpointKey: this._spoolKey,
772
+ updatedAt: this._now(),
773
+ queue: this._queue,
774
+ })}\n`,
775
+ { encoding: "utf8", mode: 0o600 },
776
+ );
777
+ this._fs.renameSync(temporary, this._spoolFile);
778
+ } catch (error) {
779
+ this._stats.spoolErrors++;
780
+ this._stats.lastError = `spool persist: ${error.message}`;
34
781
  }
35
782
  }
36
783
 
37
- _startFlushTimer() {
38
- if (this._timer) clearInterval(this._timer);
39
- this._timer = setInterval(() => this.flush(), this.flushIntervalMs);
40
- this._timer.unref?.();
784
+ _push(item, { recovered = false } = {}) {
785
+ if (!item || !SIGNALS.has(item.signal) || !item.payload) return false;
786
+ while (this._queue.length >= this.config.maxQueueSize) {
787
+ this._queue.shift();
788
+ this._stats.dropped++;
789
+ }
790
+ this._queue.push({
791
+ id: item.id || randomUUID(),
792
+ signal: item.signal,
793
+ payload: item.payload,
794
+ attempts: Number(item.attempts) || 0,
795
+ nextAttemptAt: Number(item.nextAttemptAt) || 0,
796
+ enqueuedAt: Number(item.enqueuedAt) || this._now(),
797
+ });
798
+ if (recovered) this._stats.recovered++;
799
+ else this._stats.enqueued++;
800
+ return true;
41
801
  }
42
802
 
43
- _getHttpModule(urlStr) {
44
- return urlStr.startsWith("https:") ? https : http;
803
+ enqueue(signal, payload, { redact = true } = {}) {
804
+ if (!this.enabled || !SIGNALS.has(signal)) return false;
805
+ const target = this.config[signal];
806
+ if (!target?.endpoint) return false;
807
+ const accepted = this._push({
808
+ signal,
809
+ payload: cloneAndRedact(payload, redact),
810
+ attempts: 0,
811
+ nextAttemptAt: 0,
812
+ enqueuedAt: this._now(),
813
+ });
814
+ this._persist();
815
+ if (this._queue.length >= this.config.maxExportBatchSize) {
816
+ void this.flush();
817
+ }
818
+ return accepted;
819
+ }
820
+
821
+ exportTracePayload(payload, options) {
822
+ return this.enqueue("traces", payload, options);
823
+ }
824
+
825
+ exportMetricsPayload(payload, options) {
826
+ return this.enqueue("metrics", payload, options);
827
+ }
828
+
829
+ exportSpans(spans, { redact = true } = {}) {
830
+ if (!Array.isArray(spans) || spans.length === 0) return false;
831
+ const attrs = resourceAttributes(this.config.resourceAttributes, redact);
832
+ const payload = {
833
+ resourceSpans: [
834
+ {
835
+ resource: { attributes: attrs },
836
+ scopeSpans: [
837
+ {
838
+ scope: { name: "chainlesschain.cli" },
839
+ spans: spans.map((span) => this._toOtlpSpan(span, redact)),
840
+ },
841
+ ],
842
+ },
843
+ ],
844
+ };
845
+ return this.exportTracePayload(payload, { redact });
846
+ }
847
+
848
+ _toOtlpSpan(span, redact = true) {
849
+ const start = Number(span.startTime) || this._now();
850
+ const end = Number(span.endTime) || this._now();
851
+ const statusCode =
852
+ span.status?.code === "ERROR" || span.status?.code === 2 || span.error
853
+ ? 2
854
+ : 1;
855
+ return {
856
+ traceId: span.traceId || "",
857
+ spanId: span.spanId || span.id || "",
858
+ parentSpanId: span.parentSpanId || span.parentId || "",
859
+ name: span.name || "unknown",
860
+ kind: Number(span.kind) || 1,
861
+ startTimeUnixNano: String(Math.round(start * 1e6)),
862
+ endTimeUnixNano: String(Math.round(end * 1e6)),
863
+ attributes: resourceAttributes(
864
+ span.attributes || span.metadata || {},
865
+ redact,
866
+ ),
867
+ status: {
868
+ code: statusCode,
869
+ ...(span.status?.message
870
+ ? {
871
+ message: redact
872
+ ? redactSecrets(String(span.status.message))
873
+ : String(span.status.message),
874
+ }
875
+ : {}),
876
+ },
877
+ events: (span.events || []).map((event) => ({
878
+ name: event.name || "event",
879
+ timeUnixNano: String(
880
+ Math.round((Number(event.time) || this._now()) * 1e6),
881
+ ),
882
+ attributes: resourceAttributes(event.attributes || {}, redact),
883
+ })),
884
+ };
885
+ }
886
+
887
+ exportMetrics(metrics, { redact = true } = {}) {
888
+ if (!Array.isArray(metrics) || metrics.length === 0) return false;
889
+ const nowNano = String(Math.round(this._now() * 1e6));
890
+ const payload = {
891
+ resourceMetrics: [
892
+ {
893
+ resource: {
894
+ attributes: resourceAttributes(
895
+ this.config.resourceAttributes,
896
+ redact,
897
+ ),
898
+ },
899
+ scopeMetrics: [
900
+ {
901
+ scope: { name: "chainlesschain.cli" },
902
+ metrics: metrics.map((metric) =>
903
+ this._toOtlpMetric(metric, nowNano, redact),
904
+ ),
905
+ },
906
+ ],
907
+ },
908
+ ],
909
+ };
910
+ return this.exportMetricsPayload(payload, { redact });
911
+ }
912
+
913
+ _toOtlpMetric(metric, nowNano, redact = true) {
914
+ const point = {
915
+ attributes: resourceAttributes(
916
+ metric.attributes || metric.labels || {},
917
+ redact,
918
+ ),
919
+ timeUnixNano: String(metric.timeUnixNano || nowNano),
920
+ };
921
+ if (metric.type === "histogram") {
922
+ return {
923
+ name: metric.name,
924
+ description: metric.description || "",
925
+ unit: metric.unit || "1",
926
+ histogram: {
927
+ aggregationTemporality: 2,
928
+ dataPoints: [
929
+ {
930
+ ...point,
931
+ count: String(metric.count ?? metric.value?.count ?? 0),
932
+ sum: Number(metric.sum ?? metric.value?.sum ?? 0),
933
+ bucketCounts: (metric.bucketCounts || []).map(String),
934
+ explicitBounds: metric.explicitBounds || [],
935
+ },
936
+ ],
937
+ },
938
+ };
939
+ }
940
+ const number =
941
+ typeof metric.value === "bigint"
942
+ ? { asInt: metric.value.toString() }
943
+ : Number.isInteger(metric.value)
944
+ ? { asInt: String(metric.value) }
945
+ : { asDouble: Number(metric.value) || 0 };
946
+ const data = { dataPoints: [{ ...point, ...number }] };
947
+ if (metric.type === "sum" || metric.type === "counter") {
948
+ data.aggregationTemporality = 2;
949
+ data.isMonotonic = metric.monotonic !== false;
950
+ return {
951
+ name: metric.name,
952
+ description: metric.description || "",
953
+ unit: metric.unit || "1",
954
+ sum: data,
955
+ };
956
+ }
957
+ return {
958
+ name: metric.name,
959
+ description: metric.description || "",
960
+ unit: metric.unit || "1",
961
+ gauge: data,
962
+ };
45
963
  }
46
964
 
47
- _post(path, data, contentType) {
48
- if (!this._enabled) return Promise.resolve();
965
+ async _postHttp(signal, payload) {
966
+ const target = this.config[signal];
967
+ if (!target?.endpoint) {
968
+ return { ok: false, retryable: false, error: "endpoint not configured" };
969
+ }
49
970
  return new Promise((resolve) => {
971
+ let settled = false;
972
+ const finish = (value) => {
973
+ if (settled) return;
974
+ settled = true;
975
+ resolve(value);
976
+ };
50
977
  try {
51
- const url = new URL(path, this.endpoint);
52
- const mod = this._getHttpModule(url.protocol);
53
- const payload = JSON.stringify(data);
54
- const req = mod.request(
978
+ const url = new URL(target.endpoint);
979
+ const protobuf = target.protocol === "http/protobuf";
980
+ const raw = protobuf
981
+ ? encodeOtlpProtobuf(signal, payload)
982
+ : Buffer.from(JSON.stringify(payload), "utf8");
983
+ const compressed = target.compression === "gzip";
984
+ const body = compressed ? gzipSync(raw) : raw;
985
+ const mod = url.protocol === "https:" ? this._https : this._http;
986
+ const request = mod.request(
55
987
  {
988
+ protocol: url.protocol,
56
989
  hostname: url.hostname,
57
- port: url.port,
58
- path: url.pathname + url.search,
990
+ port: url.port || undefined,
991
+ path: `${url.pathname}${url.search}`,
59
992
  method: "POST",
60
993
  headers: {
61
- "Content-Type": contentType,
62
- "Content-Length": Buffer.byteLength(payload),
63
- ...this.headers,
994
+ "Content-Type": protobuf
995
+ ? "application/x-protobuf"
996
+ : "application/json",
997
+ "Content-Length": String(body.length),
998
+ ...(compressed ? { "Content-Encoding": "gzip" } : {}),
999
+ ...safeTransportHeaders(target.headers),
64
1000
  },
65
- timeout: 10000,
1001
+ timeout: target.timeoutMs,
1002
+ ...(url.protocol === "https:" ? target.tls : {}),
66
1003
  },
67
- (res) => {
68
- res.resume();
69
- if (res.statusCode >= 200 && res.statusCode < 300) {
70
- this._successfulExports++;
71
- } else {
72
- this._failedExports++;
1004
+ (response) => {
1005
+ response.resume();
1006
+ const statusCode = response.statusCode || 0;
1007
+ if (statusCode >= 200 && statusCode < 300) {
1008
+ finish({ ok: true, statusCode });
1009
+ return;
73
1010
  }
74
- resolve();
1011
+ finish({
1012
+ ok: false,
1013
+ statusCode,
1014
+ retryable: RETRYABLE_STATUS.has(statusCode) || statusCode >= 500,
1015
+ retryAfterMs: retryAfterMs(response.headers, this._now()),
1016
+ error: `HTTP ${statusCode}`,
1017
+ });
75
1018
  },
76
1019
  );
77
- req.on("error", () => {
78
- this._failedExports++;
79
- resolve();
80
- });
81
- req.on("timeout", () => {
82
- req.destroy();
83
- this._failedExports++;
84
- resolve();
1020
+ request.once("error", (error) =>
1021
+ finish({ ok: false, retryable: true, error: error.message }),
1022
+ );
1023
+ request.once("timeout", () => {
1024
+ request.destroy(new Error("OTLP export timeout"));
1025
+ finish({ ok: false, retryable: true, error: "request timeout" });
85
1026
  });
86
- req.write(payload);
87
- req.end();
88
- } catch {
89
- resolve();
1027
+ request.end(body);
1028
+ } catch (error) {
1029
+ finish({ ok: false, retryable: false, error: error.message });
90
1030
  }
91
1031
  });
92
1032
  }
93
1033
 
94
- exportSpans(spans) {
95
- if (!this._enabled) return;
96
- for (const span of spans) {
97
- if (this._spanBuffer.length >= this.batchSize) {
98
- this._spanBuffer.shift();
99
- this._droppedSpans++;
100
- }
101
- this._spanBuffer.push({
102
- resourceSpans: [
103
- {
104
- resource: {
105
- attributes: [
106
- {
107
- key: "service.name",
108
- value: { stringValue: this.serviceName },
109
- },
110
- ],
111
- },
112
- scopeSpans: [
113
- {
114
- scope: { name: span.component || "chainlesschain" },
115
- spans: [this._toOtlpSpan(span)],
116
- },
117
- ],
118
- },
119
- ],
120
- });
1034
+ async _postGrpc(signal, payload) {
1035
+ const target = this.config[signal];
1036
+ if (!target?.endpoint) {
1037
+ return { ok: false, retryable: false, error: "endpoint not configured" };
121
1038
  }
1039
+ return new Promise((resolve) => {
1040
+ let settled = false;
1041
+ let session;
1042
+ let request;
1043
+ let httpStatus = 0;
1044
+ let grpcStatus = null;
1045
+ let grpcMessage = "";
1046
+ const finish = (value) => {
1047
+ if (settled) return;
1048
+ settled = true;
1049
+ try {
1050
+ request?.close();
1051
+ } catch {
1052
+ // The stream may already be closed by the peer.
1053
+ }
1054
+ try {
1055
+ session?.close();
1056
+ } catch {
1057
+ // The connection may have failed before opening.
1058
+ }
1059
+ resolve(value);
1060
+ };
1061
+ try {
1062
+ const url = new URL(target.endpoint);
1063
+ const method =
1064
+ signal === "traces"
1065
+ ? "/opentelemetry.proto.collector.trace.v1.TraceService/Export"
1066
+ : "/opentelemetry.proto.collector.metrics.v1.MetricsService/Export";
1067
+ const basePath = url.pathname.replace(/\/+$/, "");
1068
+ const rpcPath = basePath.endsWith("/Export")
1069
+ ? basePath
1070
+ : `${basePath === "/" ? "" : basePath}${method}`;
1071
+ const message = encodeOtlpProtobuf(signal, payload);
1072
+ const compressed = target.compression === "gzip";
1073
+ const body = compressed ? gzipSync(message) : message;
1074
+ const frame = Buffer.alloc(5 + body.length);
1075
+ frame[0] = compressed ? 1 : 0;
1076
+ frame.writeUInt32BE(body.length, 1);
1077
+ body.copy(frame, 5);
1078
+ const customHeaders = safeTransportHeaders(target.headers);
1079
+ session = this._http2.connect(url.origin, {
1080
+ ...(url.protocol === "https:" ? target.tls : {}),
1081
+ });
1082
+ session.once("error", (error) =>
1083
+ finish({ ok: false, retryable: true, error: error.message }),
1084
+ );
1085
+ request = session.request({
1086
+ ":method": "POST",
1087
+ ":path": rpcPath,
1088
+ "content-type": "application/grpc",
1089
+ te: "trailers",
1090
+ ...(compressed ? { "grpc-encoding": "gzip" } : {}),
1091
+ ...customHeaders,
1092
+ });
1093
+ request.setTimeout(target.timeoutMs, () => {
1094
+ finish({ ok: false, retryable: true, error: "request timeout" });
1095
+ });
1096
+ const readStatus = (headers) => {
1097
+ if (headers[":status"] != null) {
1098
+ httpStatus = Number(headers[":status"]);
1099
+ }
1100
+ if (headers["grpc-status"] != null) {
1101
+ grpcStatus = Number(headers["grpc-status"]);
1102
+ }
1103
+ if (headers["grpc-message"] != null) {
1104
+ try {
1105
+ grpcMessage = decodeURIComponent(String(headers["grpc-message"]));
1106
+ } catch {
1107
+ grpcMessage = String(headers["grpc-message"]);
1108
+ }
1109
+ }
1110
+ };
1111
+ request.on("response", readStatus);
1112
+ request.on("trailers", readStatus);
1113
+ request.once("error", (error) =>
1114
+ finish({ ok: false, retryable: true, error: error.message }),
1115
+ );
1116
+ request.once("end", () => {
1117
+ if (httpStatus >= 200 && httpStatus < 300 && grpcStatus === 0) {
1118
+ finish({ ok: true, statusCode: httpStatus, grpcStatus: 0 });
1119
+ return;
1120
+ }
1121
+ const retryableGrpc = new Set([4, 8, 10, 13, 14]);
1122
+ finish({
1123
+ ok: false,
1124
+ statusCode: httpStatus,
1125
+ grpcStatus,
1126
+ retryable:
1127
+ retryableGrpc.has(grpcStatus) ||
1128
+ RETRYABLE_STATUS.has(httpStatus) ||
1129
+ httpStatus >= 500,
1130
+ error:
1131
+ grpcMessage ||
1132
+ (grpcStatus == null
1133
+ ? `HTTP ${httpStatus || "connection failure"}`
1134
+ : `gRPC ${grpcStatus}`),
1135
+ });
1136
+ });
1137
+ request.resume();
1138
+ request.end(frame);
1139
+ } catch (error) {
1140
+ finish({ ok: false, retryable: false, error: error.message });
1141
+ }
1142
+ });
122
1143
  }
123
1144
 
124
- _toOtlpSpan(span) {
125
- return {
126
- traceId: span.traceId || "",
127
- spanId: span.spanId || "",
128
- parentSpanId: span.parentSpanId || "",
129
- name: span.name || "unknown",
130
- kind: this._toSpanKind(span.kind),
131
- startTimeUnixNano: (span.startTime || Date.now()) * 1_000_000,
132
- endTimeUnixNano: (span.endTime || Date.now()) * 1_000_000,
133
- attributes: Object.entries(span.attributes || {}).map(([k, v]) => ({
134
- key: k,
135
- value: this._toAttrValue(v),
136
- })),
137
- status: { code: span.error ? 2 : 1, message: span.error?.message || "" },
138
- events: (span.events || []).map((e) => ({
139
- timeUnixNano: (e.time || Date.now()) * 1_000_000,
140
- name: e.name,
141
- attributes: Object.entries(e.attributes || {}).map(([k, v]) => ({
142
- key: k,
143
- value: this._toAttrValue(v),
144
- })),
145
- })),
146
- };
147
- }
148
-
149
- _toSpanKind(kind) {
150
- const map = { internal: 0, server: 1, client: 2, producer: 3, consumer: 4 };
151
- return map[kind] ?? 0;
152
- }
153
-
154
- _toAttrValue(v) {
155
- if (typeof v === "string") return { stringValue: v };
156
- if (typeof v === "number")
157
- return Number.isInteger(v) ? { intValue: String(v) } : { doubleValue: v };
158
- if (typeof v === "boolean") return { boolValue: v };
159
- return { stringValue: String(v) };
1145
+ async _post(signal, payload) {
1146
+ return this.config[signal]?.protocol === "grpc"
1147
+ ? this._postGrpc(signal, payload)
1148
+ : this._postHttp(signal, payload);
160
1149
  }
161
1150
 
162
- exportMetrics(metrics) {
163
- if (!this._enabled) return;
164
- for (const metric of metrics) {
165
- if (this._metricBuffer.length >= this.batchSize) {
166
- this._metricBuffer.shift();
167
- this._droppedMetrics++;
1151
+ async _flush({ force = false } = {}) {
1152
+ const now = this._now();
1153
+ const due = this._queue
1154
+ .filter((item) => force || item.nextAttemptAt <= now)
1155
+ .slice(0, this.config.maxExportBatchSize);
1156
+ if (due.length === 0) return this.getStats();
1157
+ let batchFailed = false;
1158
+ for (const signal of SIGNALS) {
1159
+ const batch = due.filter((item) => item.signal === signal);
1160
+ if (batch.length === 0) continue;
1161
+ const result = await this._post(
1162
+ signal,
1163
+ mergePayloads(
1164
+ signal,
1165
+ batch.map((item) => item.payload),
1166
+ ),
1167
+ );
1168
+ if (result.ok) {
1169
+ const ids = new Set(batch.map((item) => item.id));
1170
+ this._queue = this._queue.filter((item) => !ids.has(item.id));
1171
+ this._stats.exported += batch.length;
1172
+ continue;
1173
+ }
1174
+ batchFailed = true;
1175
+ this._stats.lastError = result.error || "OTLP export failed";
1176
+ const idsToDrop = new Set();
1177
+ for (const item of batch) {
1178
+ item.attempts += 1;
1179
+ if (!result.retryable || item.attempts >= this.config.maxAttempts) {
1180
+ idsToDrop.add(item.id);
1181
+ this._stats.permanentFailures++;
1182
+ continue;
1183
+ }
1184
+ item.nextAttemptAt =
1185
+ this._now() +
1186
+ (result.retryAfterMs == null
1187
+ ? backoffMs(item.attempts)
1188
+ : result.retryAfterMs);
1189
+ this._stats.retried++;
1190
+ }
1191
+ if (idsToDrop.size > 0) {
1192
+ this._queue = this._queue.filter((item) => !idsToDrop.has(item.id));
168
1193
  }
169
- this._metricBuffer.push({
170
- resourceMetrics: [
171
- {
172
- resource: {
173
- attributes: [
174
- {
175
- key: "service.name",
176
- value: { stringValue: this.serviceName },
177
- },
178
- ],
179
- },
180
- scopeMetrics: [
181
- {
182
- scope: { name: metric.component || "chainlesschain" },
183
- metrics: [this._toOtlpMetric(metric)],
184
- },
185
- ],
186
- },
187
- ],
188
- });
189
1194
  }
1195
+ if (!batchFailed) this._stats.lastError = null;
1196
+ this._persist();
1197
+ return this.getStats();
190
1198
  }
191
1199
 
192
- _toOtlpMetric(metric) {
193
- return {
194
- name: metric.name,
195
- description: metric.description || "",
196
- unit: metric.unit || "1",
197
- [metric.type === "histogram"
198
- ? "histogram"
199
- : metric.type === "sum"
200
- ? "sum"
201
- : "gauge"]: {
202
- dataPoints: [
203
- {
204
- attributes: Object.entries(metric.attributes || {}).map(
205
- ([k, v]) => ({ key: k, value: this._toAttrValue(v) }),
206
- ),
207
- timeUnixNano: Date.now() * 1_000_000,
208
- ...(metric.type === "histogram"
209
- ? {
210
- count: metric.value?.count || 0,
211
- sum: metric.value?.sum || 0,
212
- bucketCounts: metric.value?.bucketCounts || [],
213
- explicitBounds: metric.value?.explicitBounds || [],
214
- }
215
- : { asDouble: metric.value || 0 }),
216
- },
217
- ],
218
- aggregationTemporality: 1,
219
- isMonotonic: metric.type === "sum",
220
- },
221
- };
1200
+ flush(options = {}) {
1201
+ if (!this.enabled) return Promise.resolve(this.getStats());
1202
+ if (!this._flushPromise) {
1203
+ this._flushPromise = this._flush(options).finally(() => {
1204
+ this._flushPromise = null;
1205
+ });
1206
+ }
1207
+ return this._flushPromise;
222
1208
  }
223
1209
 
224
- async flush() {
225
- if (!this._enabled) return;
226
- const spans = this._spanBuffer.splice(0);
227
- const metrics = this._metricBuffer.splice(0);
228
- const promises = [];
229
- for (const span of spans)
230
- promises.push(this._post("/v1/traces", span, "application/json"));
231
- for (const metric of metrics)
232
- promises.push(this._post("/v1/metrics", metric, "application/json"));
233
- await Promise.allSettled(promises);
1210
+ async forceFlush({ timeoutMs = 2_000 } = {}) {
1211
+ if (!this.enabled) return this.getStats();
1212
+ const deadline = this._now() + Math.max(0, timeoutMs);
1213
+ while (this._queue.length > 0 && this._now() <= deadline) {
1214
+ const before = this._queue.length;
1215
+ await this.flush({ force: true });
1216
+ if (this._queue.length === 0) break;
1217
+ if (this._queue.length >= before) {
1218
+ const wait = Math.min(100, Math.max(0, deadline - this._now()));
1219
+ if (wait === 0) break;
1220
+ await this._sleep(wait);
1221
+ }
1222
+ }
1223
+ this._persist();
1224
+ return this.getStats();
234
1225
  }
235
1226
 
236
- shutdown() {
237
- if (this._timer) clearInterval(this._timer);
238
- return this.flush();
1227
+ async shutdown(options = {}) {
1228
+ if (this._closed) return this.getStats();
1229
+ if (this._timer) this._clearInterval(this._timer);
1230
+ await this.forceFlush(options);
1231
+ this._closed = true;
1232
+ this._persist();
1233
+ return this.getStats();
239
1234
  }
240
1235
 
241
1236
  getStats() {
1237
+ const queued = this._queue.length;
1238
+ const max = this.config.maxQueueSize;
242
1239
  return {
243
- enabled: this._enabled,
244
- bufferedSpans: this._spanBuffer.length,
245
- bufferedMetrics: this._metricBuffer.length,
246
- droppedSpans: this._droppedSpans,
247
- droppedMetrics: this._droppedMetrics,
248
- failedExports: this._failedExports,
249
- successfulExports: this._successfulExports,
1240
+ enabled: this.enabled,
1241
+ protocol:
1242
+ this.config.traces.protocol === this.config.metrics.protocol
1243
+ ? this.config.traces.protocol
1244
+ : "mixed",
1245
+ tracesProtocol: this.config.traces.protocol,
1246
+ metricsProtocol: this.config.metrics.protocol,
1247
+ tracesEndpoint: this.config.traces.endpoint,
1248
+ metricsEndpoint: this.config.metrics.endpoint,
1249
+ queued,
1250
+ queueCapacity: max,
1251
+ queuePressure:
1252
+ queued >= max
1253
+ ? "full"
1254
+ : queued >= Math.ceil(max * 0.8)
1255
+ ? "high"
1256
+ : "normal",
1257
+ ...this._stats,
250
1258
  };
251
1259
  }
252
1260
  }
253
1261
 
254
- let _defaultExporter = null;
255
- export function getDefaultOtlpExporter() {
256
- if (!_defaultExporter) _defaultExporter = new OtlpExporter();
257
- return _defaultExporter;
258
- }
259
- export { OtlpExporter };
260
1262
  export default OtlpExporter;