autotel 2.1.0

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 (272) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1946 -0
  3. package/dist/chunk-2LNRY4QK.js +273 -0
  4. package/dist/chunk-2LNRY4QK.js.map +1 -0
  5. package/dist/chunk-3HENGDW2.js +587 -0
  6. package/dist/chunk-3HENGDW2.js.map +1 -0
  7. package/dist/chunk-4OAT42CA.cjs +73 -0
  8. package/dist/chunk-4OAT42CA.cjs.map +1 -0
  9. package/dist/chunk-5GWX5LFW.js +70 -0
  10. package/dist/chunk-5GWX5LFW.js.map +1 -0
  11. package/dist/chunk-5R2M36QB.js +195 -0
  12. package/dist/chunk-5R2M36QB.js.map +1 -0
  13. package/dist/chunk-5ZN622AO.js +73 -0
  14. package/dist/chunk-5ZN622AO.js.map +1 -0
  15. package/dist/chunk-77MSMAUQ.cjs +498 -0
  16. package/dist/chunk-77MSMAUQ.cjs.map +1 -0
  17. package/dist/chunk-ABPEQ6RK.cjs +596 -0
  18. package/dist/chunk-ABPEQ6RK.cjs.map +1 -0
  19. package/dist/chunk-BWYGJKRB.js +95 -0
  20. package/dist/chunk-BWYGJKRB.js.map +1 -0
  21. package/dist/chunk-BZHG5IZ4.js +73 -0
  22. package/dist/chunk-BZHG5IZ4.js.map +1 -0
  23. package/dist/chunk-G7VZBCD6.cjs +35 -0
  24. package/dist/chunk-G7VZBCD6.cjs.map +1 -0
  25. package/dist/chunk-GVLK7YUU.cjs +30 -0
  26. package/dist/chunk-GVLK7YUU.cjs.map +1 -0
  27. package/dist/chunk-HCCXC7XG.js +205 -0
  28. package/dist/chunk-HCCXC7XG.js.map +1 -0
  29. package/dist/chunk-HE6T6FIX.cjs +203 -0
  30. package/dist/chunk-HE6T6FIX.cjs.map +1 -0
  31. package/dist/chunk-KIXWPOCO.cjs +100 -0
  32. package/dist/chunk-KIXWPOCO.cjs.map +1 -0
  33. package/dist/chunk-KVGNW3FC.js +87 -0
  34. package/dist/chunk-KVGNW3FC.js.map +1 -0
  35. package/dist/chunk-LITNXTTT.js +3 -0
  36. package/dist/chunk-LITNXTTT.js.map +1 -0
  37. package/dist/chunk-M4ANN7RL.js +114 -0
  38. package/dist/chunk-M4ANN7RL.js.map +1 -0
  39. package/dist/chunk-NC52UBR2.cjs +32 -0
  40. package/dist/chunk-NC52UBR2.cjs.map +1 -0
  41. package/dist/chunk-NHCNRQD3.cjs +212 -0
  42. package/dist/chunk-NHCNRQD3.cjs.map +1 -0
  43. package/dist/chunk-NZ72VDNY.cjs +4 -0
  44. package/dist/chunk-NZ72VDNY.cjs.map +1 -0
  45. package/dist/chunk-P6JUDYNO.js +57 -0
  46. package/dist/chunk-P6JUDYNO.js.map +1 -0
  47. package/dist/chunk-RJYY7BWX.js +1349 -0
  48. package/dist/chunk-RJYY7BWX.js.map +1 -0
  49. package/dist/chunk-TRI4V5BF.cjs +126 -0
  50. package/dist/chunk-TRI4V5BF.cjs.map +1 -0
  51. package/dist/chunk-UL33I6IS.js +139 -0
  52. package/dist/chunk-UL33I6IS.js.map +1 -0
  53. package/dist/chunk-URRW6M2C.cjs +61 -0
  54. package/dist/chunk-URRW6M2C.cjs.map +1 -0
  55. package/dist/chunk-UY3UYPBZ.cjs +77 -0
  56. package/dist/chunk-UY3UYPBZ.cjs.map +1 -0
  57. package/dist/chunk-W3253FGB.cjs +277 -0
  58. package/dist/chunk-W3253FGB.cjs.map +1 -0
  59. package/dist/chunk-W7LHZVQF.js +26 -0
  60. package/dist/chunk-W7LHZVQF.js.map +1 -0
  61. package/dist/chunk-WBWNM6LB.cjs +1360 -0
  62. package/dist/chunk-WBWNM6LB.cjs.map +1 -0
  63. package/dist/chunk-WFJ7L2RV.js +494 -0
  64. package/dist/chunk-WFJ7L2RV.js.map +1 -0
  65. package/dist/chunk-X4RMFFMR.js +28 -0
  66. package/dist/chunk-X4RMFFMR.js.map +1 -0
  67. package/dist/chunk-Y4Y2S7BM.cjs +92 -0
  68. package/dist/chunk-Y4Y2S7BM.cjs.map +1 -0
  69. package/dist/chunk-YLPNXZFI.cjs +143 -0
  70. package/dist/chunk-YLPNXZFI.cjs.map +1 -0
  71. package/dist/chunk-YTXEZ4SD.cjs +77 -0
  72. package/dist/chunk-YTXEZ4SD.cjs.map +1 -0
  73. package/dist/chunk-Z6ZWNWWR.js +30 -0
  74. package/dist/chunk-Z6ZWNWWR.js.map +1 -0
  75. package/dist/config.cjs +26 -0
  76. package/dist/config.cjs.map +1 -0
  77. package/dist/config.d.cts +75 -0
  78. package/dist/config.d.ts +75 -0
  79. package/dist/config.js +5 -0
  80. package/dist/config.js.map +1 -0
  81. package/dist/db.cjs +233 -0
  82. package/dist/db.cjs.map +1 -0
  83. package/dist/db.d.cts +123 -0
  84. package/dist/db.d.ts +123 -0
  85. package/dist/db.js +228 -0
  86. package/dist/db.js.map +1 -0
  87. package/dist/decorators.cjs +67 -0
  88. package/dist/decorators.cjs.map +1 -0
  89. package/dist/decorators.d.cts +91 -0
  90. package/dist/decorators.d.ts +91 -0
  91. package/dist/decorators.js +65 -0
  92. package/dist/decorators.js.map +1 -0
  93. package/dist/event-subscriber.cjs +6 -0
  94. package/dist/event-subscriber.cjs.map +1 -0
  95. package/dist/event-subscriber.d.cts +116 -0
  96. package/dist/event-subscriber.d.ts +116 -0
  97. package/dist/event-subscriber.js +3 -0
  98. package/dist/event-subscriber.js.map +1 -0
  99. package/dist/event-testing.cjs +21 -0
  100. package/dist/event-testing.cjs.map +1 -0
  101. package/dist/event-testing.d.cts +110 -0
  102. package/dist/event-testing.d.ts +110 -0
  103. package/dist/event-testing.js +4 -0
  104. package/dist/event-testing.js.map +1 -0
  105. package/dist/event.cjs +30 -0
  106. package/dist/event.cjs.map +1 -0
  107. package/dist/event.d.cts +282 -0
  108. package/dist/event.d.ts +282 -0
  109. package/dist/event.js +13 -0
  110. package/dist/event.js.map +1 -0
  111. package/dist/exporters.cjs +17 -0
  112. package/dist/exporters.cjs.map +1 -0
  113. package/dist/exporters.d.cts +1 -0
  114. package/dist/exporters.d.ts +1 -0
  115. package/dist/exporters.js +4 -0
  116. package/dist/exporters.js.map +1 -0
  117. package/dist/functional.cjs +46 -0
  118. package/dist/functional.cjs.map +1 -0
  119. package/dist/functional.d.cts +478 -0
  120. package/dist/functional.d.ts +478 -0
  121. package/dist/functional.js +13 -0
  122. package/dist/functional.js.map +1 -0
  123. package/dist/http.cjs +189 -0
  124. package/dist/http.cjs.map +1 -0
  125. package/dist/http.d.cts +169 -0
  126. package/dist/http.d.ts +169 -0
  127. package/dist/http.js +184 -0
  128. package/dist/http.js.map +1 -0
  129. package/dist/index.cjs +333 -0
  130. package/dist/index.cjs.map +1 -0
  131. package/dist/index.d.cts +758 -0
  132. package/dist/index.d.ts +758 -0
  133. package/dist/index.js +143 -0
  134. package/dist/index.js.map +1 -0
  135. package/dist/instrumentation.cjs +182 -0
  136. package/dist/instrumentation.cjs.map +1 -0
  137. package/dist/instrumentation.d.cts +49 -0
  138. package/dist/instrumentation.d.ts +49 -0
  139. package/dist/instrumentation.js +179 -0
  140. package/dist/instrumentation.js.map +1 -0
  141. package/dist/logger.cjs +19 -0
  142. package/dist/logger.cjs.map +1 -0
  143. package/dist/logger.d.cts +146 -0
  144. package/dist/logger.d.ts +146 -0
  145. package/dist/logger.js +6 -0
  146. package/dist/logger.js.map +1 -0
  147. package/dist/metric-helpers.cjs +31 -0
  148. package/dist/metric-helpers.cjs.map +1 -0
  149. package/dist/metric-helpers.d.cts +13 -0
  150. package/dist/metric-helpers.d.ts +13 -0
  151. package/dist/metric-helpers.js +6 -0
  152. package/dist/metric-helpers.js.map +1 -0
  153. package/dist/metric-testing.cjs +21 -0
  154. package/dist/metric-testing.cjs.map +1 -0
  155. package/dist/metric-testing.d.cts +110 -0
  156. package/dist/metric-testing.d.ts +110 -0
  157. package/dist/metric-testing.js +4 -0
  158. package/dist/metric-testing.js.map +1 -0
  159. package/dist/metric.cjs +26 -0
  160. package/dist/metric.cjs.map +1 -0
  161. package/dist/metric.d.cts +240 -0
  162. package/dist/metric.d.ts +240 -0
  163. package/dist/metric.js +9 -0
  164. package/dist/metric.js.map +1 -0
  165. package/dist/processors.cjs +17 -0
  166. package/dist/processors.cjs.map +1 -0
  167. package/dist/processors.d.cts +1 -0
  168. package/dist/processors.d.ts +1 -0
  169. package/dist/processors.js +4 -0
  170. package/dist/processors.js.map +1 -0
  171. package/dist/sampling.cjs +40 -0
  172. package/dist/sampling.cjs.map +1 -0
  173. package/dist/sampling.d.cts +260 -0
  174. package/dist/sampling.d.ts +260 -0
  175. package/dist/sampling.js +7 -0
  176. package/dist/sampling.js.map +1 -0
  177. package/dist/semantic-helpers.cjs +35 -0
  178. package/dist/semantic-helpers.cjs.map +1 -0
  179. package/dist/semantic-helpers.d.cts +442 -0
  180. package/dist/semantic-helpers.d.ts +442 -0
  181. package/dist/semantic-helpers.js +14 -0
  182. package/dist/semantic-helpers.js.map +1 -0
  183. package/dist/tail-sampling-processor.cjs +13 -0
  184. package/dist/tail-sampling-processor.cjs.map +1 -0
  185. package/dist/tail-sampling-processor.d.cts +27 -0
  186. package/dist/tail-sampling-processor.d.ts +27 -0
  187. package/dist/tail-sampling-processor.js +4 -0
  188. package/dist/tail-sampling-processor.js.map +1 -0
  189. package/dist/testing.cjs +286 -0
  190. package/dist/testing.cjs.map +1 -0
  191. package/dist/testing.d.cts +291 -0
  192. package/dist/testing.d.ts +291 -0
  193. package/dist/testing.js +263 -0
  194. package/dist/testing.js.map +1 -0
  195. package/dist/trace-context-DRZdUvVY.d.cts +181 -0
  196. package/dist/trace-context-DRZdUvVY.d.ts +181 -0
  197. package/dist/trace-helpers.cjs +54 -0
  198. package/dist/trace-helpers.cjs.map +1 -0
  199. package/dist/trace-helpers.d.cts +524 -0
  200. package/dist/trace-helpers.d.ts +524 -0
  201. package/dist/trace-helpers.js +5 -0
  202. package/dist/trace-helpers.js.map +1 -0
  203. package/dist/tracer-provider.cjs +21 -0
  204. package/dist/tracer-provider.cjs.map +1 -0
  205. package/dist/tracer-provider.d.cts +169 -0
  206. package/dist/tracer-provider.d.ts +169 -0
  207. package/dist/tracer-provider.js +4 -0
  208. package/dist/tracer-provider.js.map +1 -0
  209. package/package.json +280 -0
  210. package/src/baggage-span-processor.test.ts +202 -0
  211. package/src/baggage-span-processor.ts +98 -0
  212. package/src/circuit-breaker.test.ts +341 -0
  213. package/src/circuit-breaker.ts +184 -0
  214. package/src/config.test.ts +94 -0
  215. package/src/config.ts +169 -0
  216. package/src/db.test.ts +252 -0
  217. package/src/db.ts +447 -0
  218. package/src/decorators.test.ts +203 -0
  219. package/src/decorators.ts +188 -0
  220. package/src/env-config.test.ts +246 -0
  221. package/src/env-config.ts +158 -0
  222. package/src/event-queue.test.ts +222 -0
  223. package/src/event-queue.ts +203 -0
  224. package/src/event-subscriber.ts +136 -0
  225. package/src/event-testing.ts +197 -0
  226. package/src/event.test.ts +718 -0
  227. package/src/event.ts +556 -0
  228. package/src/exporters.ts +96 -0
  229. package/src/functional.test.ts +1059 -0
  230. package/src/functional.ts +2295 -0
  231. package/src/http.test.ts +487 -0
  232. package/src/http.ts +424 -0
  233. package/src/index.ts +158 -0
  234. package/src/init.customization.test.ts +210 -0
  235. package/src/init.integrations.test.ts +366 -0
  236. package/src/init.openllmetry.test.ts +282 -0
  237. package/src/init.protocol.test.ts +215 -0
  238. package/src/init.ts +1426 -0
  239. package/src/instrumentation.test.ts +108 -0
  240. package/src/instrumentation.ts +308 -0
  241. package/src/logger.test.ts +117 -0
  242. package/src/logger.ts +246 -0
  243. package/src/metric-helpers.ts +47 -0
  244. package/src/metric-testing.ts +197 -0
  245. package/src/metric.ts +434 -0
  246. package/src/metrics.test.ts +205 -0
  247. package/src/operation-context.ts +93 -0
  248. package/src/processors.ts +106 -0
  249. package/src/rate-limiter.test.ts +199 -0
  250. package/src/rate-limiter.ts +98 -0
  251. package/src/sampling.test.ts +513 -0
  252. package/src/sampling.ts +428 -0
  253. package/src/semantic-helpers.test.ts +311 -0
  254. package/src/semantic-helpers.ts +584 -0
  255. package/src/shutdown.test.ts +311 -0
  256. package/src/shutdown.ts +222 -0
  257. package/src/stub.integration.test.ts +361 -0
  258. package/src/tail-sampling-processor.test.ts +226 -0
  259. package/src/tail-sampling-processor.ts +51 -0
  260. package/src/testing.ts +670 -0
  261. package/src/trace-context.ts +470 -0
  262. package/src/trace-helpers.new.test.ts +278 -0
  263. package/src/trace-helpers.test.ts +242 -0
  264. package/src/trace-helpers.ts +690 -0
  265. package/src/tracer-provider.test.ts +183 -0
  266. package/src/tracer-provider.ts +266 -0
  267. package/src/track.test.ts +153 -0
  268. package/src/track.ts +120 -0
  269. package/src/validation.test.ts +306 -0
  270. package/src/validation.ts +239 -0
  271. package/src/variable-name-inference.test.ts +178 -0
  272. package/src/variable-name-inference.ts +242 -0
package/README.md ADDED
@@ -0,0 +1,1946 @@
1
+ # 🔭 autotel
2
+
3
+ [![npm version](https://img.shields.io/npm/v/autotel.svg?label=autotel)](https://www.npmjs.com/package/autotel)
4
+ [![npm subscribers](https://img.shields.io/npm/v/autotel-subscribers.svg?label=subscribers)](https://www.npmjs.com/package/autotel-subscribers)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
6
+
7
+ **Write once, observe everywhere.** Instrument your Node.js code a single time, keep the DX you love, and stream traces, metrics, logs, and product events to **any** observability stack without vendor lock-in.
8
+
9
+ - **Drop-in DX** – one `init()` and ergonomic helpers like `trace()`, `span()`, `withTracing()`, decorators, and batch instrumentation.
10
+ - **Platform freedom** – OTLP-first design plus subscribers for PostHog, Mixpanel, Amplitude, and anything else via custom exporters/readers.
11
+ - **Production hardening** – adaptive sampling (10% baseline, 100% errors/slow paths), rate limiting, circuit breakers, payload validation, and automatic sensitive-field redaction.
12
+ - **Auto enrichment** – service metadata, deployment info, and AsyncLocalStorage-powered correlation IDs automatically flow into spans, metrics, logs, and events.
13
+
14
+ > Raw OpenTelemetry is verbose, and vendor SDKs create lock-in. Autotel gives you the best parts of both: clean ergonomics **and** total ownership of your telemetry.
15
+
16
+ ## Migrating from OpenTelemetry?
17
+
18
+ **[Migration Guide](../../docs/MIGRATION.md)** - Pattern-by-pattern migration walkthrough with side-by-side comparisons and deployment checklist.
19
+
20
+ Replace `NODE_OPTIONS` and 30+ lines of SDK boilerplate with `init()`, wrap functions with `trace()` instead of manual `span.start()`/`span.end()`.
21
+
22
+ ---
23
+
24
+ ## Table of Contents
25
+
26
+ - [🔭 autotel](#-autotel)
27
+ - [Migrating from OpenTelemetry?](#migrating-from-opentelemetry)
28
+ - [Table of Contents](#table-of-contents)
29
+ - [Why Autotel](#why-autotel)
30
+ - [Quick Start](#quick-start)
31
+ - [1. Install](#1-install)
32
+ - [2. Initialize once at startup](#2-initialize-once-at-startup)
33
+ - [3. Instrument code with `trace()`](#3-instrument-code-with-trace)
34
+ - [4. See the value everywhere](#4-see-the-value-everywhere)
35
+ - [Choose Any Destination](#choose-any-destination)
36
+ - [LLM Observability with OpenLLMetry](#llm-observability-with-openllmetry)
37
+ - [Installation](#installation)
38
+ - [Usage](#usage)
39
+ - [Core Building Blocks](#core-building-blocks)
40
+ - [trace()](#trace)
41
+ - [span()](#span)
42
+ - [Trace Context (`ctx`)](#trace-context-ctx)
43
+ - [Reusable Middleware Helpers](#reusable-middleware-helpers)
44
+ - [Decorators (TypeScript 5+)](#decorators-typescript-5)
45
+ - [Database Instrumentation](#database-instrumentation)
46
+ - [Business Metrics \& Product Events](#business-metrics--product-events)
47
+ - [OpenTelemetry Metrics (Metric class + helpers)](#opentelemetry-metrics-metric-class--helpers)
48
+ - [Product Events (PostHog, Mixpanel, Amplitude, …)](#product-events-posthog-mixpanel-amplitude-)
49
+ - [Logging with Trace Context](#logging-with-trace-context)
50
+ - [Using Pino (recommended)](#using-pino-recommended)
51
+ - [Using Winston](#using-winston)
52
+ - [Using Bunyan (or other loggers)](#using-bunyan-or-other-loggers)
53
+ - [What you get automatically](#what-you-get-automatically)
54
+ - [Auto Instrumentation \& Advanced Configuration](#auto-instrumentation--advanced-configuration)
55
+ - [Operational Safety \& Runtime Controls](#operational-safety--runtime-controls)
56
+ - [Configuration Reference](#configuration-reference)
57
+ - [Building Custom Instrumentation](#building-custom-instrumentation)
58
+ - [Instrumenting Queue Consumers](#instrumenting-queue-consumers)
59
+ - [Instrumenting Scheduled Jobs / Cron](#instrumenting-scheduled-jobs--cron)
60
+ - [Creating Custom Event Subscribers](#creating-custom-event-subscribers)
61
+ - [Low-Level Span Manipulation](#low-level-span-manipulation)
62
+ - [Custom Metrics](#custom-metrics)
63
+ - [Serverless \& Short-lived Processes](#serverless--short-lived-processes)
64
+ - [Manual Flush (Recommended for Serverless)](#manual-flush-recommended-for-serverless)
65
+ - [Auto-Flush Spans (Opt-in)](#auto-flush-spans-opt-in)
66
+ - [Edge Runtimes (Cloudflare Workers, Vercel Edge)](#edge-runtimes-cloudflare-workers-vercel-edge)
67
+ - [API Reference](#api-reference)
68
+ - [FAQ \& Next Steps](#faq--next-steps)
69
+ - [Troubleshooting \& Debugging](#troubleshooting--debugging)
70
+ - [Quick Debug Mode (Recommended)](#quick-debug-mode-recommended)
71
+ - [Manual Configuration (Advanced)](#manual-configuration-advanced)
72
+ - [ConsoleSpanExporter (Visual Debugging)](#consolespanexporter-visual-debugging)
73
+ - [InMemorySpanExporter (Testing \& Assertions)](#inmemoryspanexporter-testing--assertions)
74
+ - [Using Both (Advanced)](#using-both-advanced)
75
+ - [Creating Custom Instrumentation](#creating-custom-instrumentation)
76
+ - [Quick Start Template](#quick-start-template)
77
+ - [Step-by-Step Tutorial: Instrumenting Axios](#step-by-step-tutorial-instrumenting-axios)
78
+ - [Best Practices](#best-practices)
79
+ - [1. Idempotent Instrumentation](#1-idempotent-instrumentation)
80
+ - [2. Error Handling](#2-error-handling)
81
+ - [3. Security - Don't Capture Sensitive Data](#3-security---dont-capture-sensitive-data)
82
+ - [4. Follow OpenTelemetry Semantic Conventions](#4-follow-opentelemetry-semantic-conventions)
83
+ - [5. Choose the Right SpanKind](#5-choose-the-right-spankind)
84
+ - [6. TypeScript Type Safety](#6-typescript-type-safety)
85
+ - [Available Utilities](#available-utilities)
86
+ - [From `autotel/trace-helpers`](#from-autoteltrace-helpers)
87
+ - [From `@opentelemetry/api`](#from-opentelemetryapi)
88
+ - [Semantic Conventions (Optional)](#semantic-conventions-optional)
89
+ - [Real-World Examples](#real-world-examples)
90
+ - [When to Create Custom Instrumentation](#when-to-create-custom-instrumentation)
91
+ - [Using Official Instrumentation](#using-official-instrumentation)
92
+
93
+ ## Why Autotel
94
+
95
+ | Challenge | With autotel |
96
+ | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
97
+ | Writing raw OpenTelemetry spans/metrics takes dozens of lines and manual lifecycle management. | Wrap any function in `trace()` or `span()` and get automatic span lifecycle, error capture, attributes, and adaptive sampling. |
98
+ | Vendor SDKs simplify setup but trap your data in a single platform. | Autotel is OTLP-native and works with Grafana Cloud, Datadog, New Relic, Tempo, Honeycomb, Elasticsearch, or your own collector. |
99
+ | Teams need both observability **and** product events. | Ship technical telemetry and funnel/behavior events through the same API with contextual enrichment. |
100
+ | Production readiness requires redaction, rate limiting, and circuit breakers. | Those guardrails are on by default so you can safely enable telemetry everywhere. |
101
+
102
+ ## Quick Start
103
+
104
+ > Want to follow along in code? This repo ships with `apps/example-basic` (mirrors the steps below) and `apps/example-http` for an Express server, you can run either with `pnpm start` after `pnpm install && pnpm build` at the root.
105
+
106
+ ### 1. Install
107
+
108
+ ```bash
109
+ npm install autotel
110
+ # or
111
+ pnpm add autotel
112
+ ```
113
+
114
+ ### 2. Initialize once at startup
115
+
116
+ ```typescript
117
+ import { init } from 'autotel';
118
+
119
+ init({
120
+ service: 'checkout-api',
121
+ environment: process.env.NODE_ENV,
122
+ });
123
+ ```
124
+
125
+ Defaults:
126
+
127
+ - OTLP endpoint: `process.env.OTLP_ENDPOINT || http://localhost:4318`
128
+ - Metrics: on in every environment
129
+ - Sampler: adaptive (10% baseline, 100% for errors/slow spans)
130
+ - Version: auto-detected from `package.json`
131
+ - Events auto-flush when the root span finishes
132
+
133
+ ### 3. Instrument code with `trace()`
134
+
135
+ ```typescript
136
+ import { trace } from 'autotel';
137
+
138
+ export const createUser = trace(async function createUser(
139
+ data: CreateUserData,
140
+ ) {
141
+ const user = await db.users.insert(data);
142
+ return user;
143
+ });
144
+ ```
145
+
146
+ - Named function expressions automatically become span names (`code.function`).
147
+ - Errors are recorded, spans are ended, and status is set automatically.
148
+
149
+ ### 4. See the value everywhere
150
+
151
+ ```typescript
152
+ import { init, track } from 'autotel';
153
+
154
+ init({
155
+ service: 'checkout-api',
156
+ endpoint: 'https://otlp-gateway-prod.grafana.net/otlp',
157
+ subscribers: [new PostHogSubscriber({ apiKey: process.env.POSTHOG_KEY! })],
158
+ });
159
+
160
+ export const processOrder = trace(async function processOrder(order) {
161
+ track('order.completed', { amount: order.total });
162
+ return charge(order);
163
+ });
164
+ ```
165
+
166
+ Every span, metric, log line, and event includes `traceId`, `spanId`, `operation.name`, `service.version`, and `deployment.environment` automatically.
167
+
168
+ ## Choose Any Destination
169
+
170
+ ```typescript
171
+ import { init } from 'autotel';
172
+
173
+ init({
174
+ service: 'my-app',
175
+ // Grafana / Tempo / OTLP collector
176
+ endpoint: 'https://otlp-gateway-prod.grafana.net/otlp',
177
+ });
178
+
179
+ init({
180
+ service: 'my-app',
181
+ // Datadog (traces + metrics + logs via OTLP)
182
+ endpoint: 'https://otlp.datadoghq.com',
183
+ otlpHeaders: 'dd-api-key=...',
184
+ });
185
+
186
+ init({
187
+ service: 'my-app',
188
+ // Honeycomb (gRPC protocol)
189
+ protocol: 'grpc',
190
+ endpoint: 'api.honeycomb.io:443',
191
+ otlpHeaders: {
192
+ 'x-honeycomb-team': process.env.HONEYCOMB_API_KEY!,
193
+ },
194
+ });
195
+
196
+ init({
197
+ service: 'my-app',
198
+ // Custom pipeline with your own exporters/readers
199
+ spanProcessor: new BatchSpanProcessor(
200
+ new JaegerExporter({ endpoint: 'http://otel:14268/api/traces' }),
201
+ ),
202
+ metricReader: new PeriodicExportingMetricReader({
203
+ exporter: new OTLPMetricExporter({
204
+ url: 'https://metrics.example.com/v1/metrics',
205
+ }),
206
+ }),
207
+ logRecordProcessors: [
208
+ new BatchLogRecordProcessor(
209
+ new OTLPLogExporter({ url: 'https://logs.example.com/v1/logs' }),
210
+ ),
211
+ ],
212
+ instrumentations: [new HttpInstrumentation()],
213
+ });
214
+
215
+ init({
216
+ service: 'my-app',
217
+ // Product events subscribers (ship alongside OTLP)
218
+ subscribers: [
219
+ new PostHogSubscriber({ apiKey: process.env.POSTHOG_KEY! }),
220
+ new MixpanelSubscriber({ projectToken: process.env.MIXPANEL_TOKEN! }),
221
+ ],
222
+ });
223
+
224
+ init({
225
+ service: 'my-app',
226
+ // OpenLLMetry integration for LLM observability
227
+ openllmetry: {
228
+ enabled: true,
229
+ options: {
230
+ disableBatch: process.env.NODE_ENV !== 'production',
231
+ apiKey: process.env.TRACELOOP_API_KEY,
232
+ },
233
+ },
234
+ });
235
+ ```
236
+
237
+ Autotel never owns your data, it's a thin layer over OpenTelemetry with optional adapters.
238
+
239
+ ## LLM Observability with OpenLLMetry
240
+
241
+ Autotel integrates seamlessly with [OpenLLMetry](https://github.com/traceloop/openllmetry) to provide comprehensive observability for LLM applications. OpenLLMetry automatically instruments LLM providers (OpenAI, Anthropic, etc.), vector databases, and frameworks (LangChain, LlamaIndex, etc.).
242
+
243
+ ### Installation
244
+
245
+ Install the OpenLLMetry SDK as an optional peer dependency:
246
+
247
+ ```bash
248
+ pnpm add @traceloop/node-server-sdk
249
+ # or
250
+ npm install @traceloop/node-server-sdk
251
+ ```
252
+
253
+ ### Usage
254
+
255
+ Enable OpenLLMetry in your autotel configuration:
256
+
257
+ ```typescript
258
+ import { init } from 'autotel';
259
+
260
+ init({
261
+ service: 'my-llm-app',
262
+ endpoint: process.env.OTLP_ENDPOINT,
263
+ openllmetry: {
264
+ enabled: true,
265
+ options: {
266
+ // Disable batching in development for immediate traces
267
+ disableBatch: process.env.NODE_ENV !== 'production',
268
+ // Optional: Traceloop API key if using Traceloop backend
269
+ apiKey: process.env.TRACELOOP_API_KEY,
270
+ },
271
+ },
272
+ });
273
+ ```
274
+
275
+ OpenLLMetry will automatically:
276
+
277
+ - Instrument LLM calls (OpenAI, Anthropic, Cohere, etc.)
278
+ - Track vector database operations (Pinecone, Chroma, Qdrant, etc.)
279
+ - Monitor LLM frameworks (LangChain, LlamaIndex, LangGraph, etc.)
280
+ - Reuse autotel's OpenTelemetry tracer provider for unified traces
281
+
282
+ All LLM spans will appear alongside your application traces in your observability backend.
283
+
284
+ **AI Workflow Patterns:** See [AI/LLM Workflow Documentation](../../docs/AI_WORKFLOWS.md) for comprehensive patterns including:
285
+
286
+ - Multi-agent workflows (orchestration and handoffs)
287
+ - RAG pipelines (embeddings, search, generation)
288
+ - Streaming responses
289
+ - Evaluation loops
290
+ - Working examples in `apps/example-ai-agent`
291
+
292
+ ## Core Building Blocks
293
+
294
+ ### trace()
295
+
296
+ Wrap any sync/async function to create spans automatically.
297
+
298
+ ```typescript
299
+ import { trace } from 'autotel';
300
+
301
+ export const updateUser = trace(async function updateUser(
302
+ id: string,
303
+ data: UserInput,
304
+ ) {
305
+ return db.users.update(id, data);
306
+ });
307
+
308
+ // Explicit name (useful for anonymous/arrow functions)
309
+ export const deleteUser = trace('user.delete', async (id: string) => {
310
+ return db.users.delete(id);
311
+ });
312
+
313
+ // Factory form exposes the `ctx` helper (see below)
314
+ export const createOrder = trace((ctx) => async (order: Order) => {
315
+ ctx.setAttribute('order.id', order.id);
316
+ return submit(order);
317
+ });
318
+
319
+ // Immediate execution - wraps and executes instantly (for middleware/wrappers)
320
+ function timed<T>(operation: string, fn: () => Promise<T>): Promise<T> {
321
+ return trace(operation, async (ctx) => {
322
+ ctx.setAttribute('operation', operation);
323
+ return await fn();
324
+ });
325
+ }
326
+ // Executes immediately, returns Promise<T> directly
327
+ ```
328
+
329
+ **Two patterns supported:**
330
+
331
+ 1. **Factory pattern** `trace(ctx => (...args) => result)` – Returns a wrapped function for reuse
332
+ 2. **Immediate execution** `trace(ctx => result)` – Executes once immediately, returns the result directly
333
+
334
+ - Automatic span lifecycle (`start`, `end`, status, and error recording).
335
+ - Function names feed `operation.name`, `code.function`, and events enrichment.
336
+ - Works with promises, async/await, or sync functions.
337
+
338
+ ### span()
339
+
340
+ Create nested spans for individual code blocks without wrapping entire functions.
341
+
342
+ ```typescript
343
+ import { span, trace } from 'autotel';
344
+
345
+ export const rollDice = trace(async function rollDice(rolls: number) {
346
+ const results: number[] = [];
347
+
348
+ for (let i = 0; i < rolls; i++) {
349
+ await span(
350
+ { name: 'roll.once', attributes: { roll: i + 1 } },
351
+ async (span) => {
352
+ span.setAttribute('range', '1-6');
353
+ span.addEvent('dice.rolled', { value: rollOnce() });
354
+ results.push(rollOnce());
355
+ },
356
+ );
357
+ }
358
+
359
+ return results;
360
+ });
361
+ ```
362
+
363
+ Nested spans automatically inherit context and correlation IDs.
364
+
365
+ ### Trace Context (`ctx`)
366
+
367
+ Every `trace((ctx) => ...)` factory receives a type-safe helper backed by `AsyncLocalStorage`.
368
+
369
+ ```typescript
370
+ export const createUser = trace((ctx) => async (input: CreateUserData) => {
371
+ logger.info('Handling request', { traceId: ctx.traceId });
372
+ ctx.setAttributes({ 'user.id': input.id, 'user.plan': input.plan });
373
+
374
+ try {
375
+ const user = await db.users.create(input);
376
+ ctx.setStatus({ code: SpanStatusCode.OK });
377
+ return user;
378
+ } catch (error) {
379
+ ctx.recordException(error as Error);
380
+ ctx.setStatus({
381
+ code: SpanStatusCode.ERROR,
382
+ message: 'Failed to create user',
383
+ });
384
+ throw error;
385
+ }
386
+ });
387
+ ```
388
+
389
+ Available helpers: `traceId`, `spanId`, `correlationId`, `setAttribute`, `setAttributes`, `setStatus`, `recordException`, `getBaggage`, `setBaggage`, `deleteBaggage`, `getAllBaggage`.
390
+
391
+ #### Baggage (Context Propagation)
392
+
393
+ Baggage allows you to propagate custom key-value pairs across distributed traces. Baggage is automatically included in HTTP headers when using `injectTraceContext()` from `autotel/http`.
394
+
395
+ ```typescript
396
+ import { trace, withBaggage } from 'autotel';
397
+ import { injectTraceContext } from 'autotel/http';
398
+
399
+ // Set baggage for downstream services
400
+ export const createOrder = trace((ctx) => async (order: Order) => {
401
+ return await withBaggage({
402
+ baggage: {
403
+ 'tenant.id': order.tenantId,
404
+ 'user.id': order.userId,
405
+ },
406
+ fn: async () => {
407
+ // Baggage is available to all child spans and HTTP calls
408
+ const tenantId = ctx.getBaggage('tenant.id');
409
+ ctx.setAttribute('tenant.id', tenantId || 'unknown');
410
+
411
+ // HTTP headers automatically include baggage
412
+ const headers = injectTraceContext();
413
+ await fetch('/api/charge', { headers, body: JSON.stringify(order) });
414
+ },
415
+ });
416
+ });
417
+ ```
418
+
419
+ **Typed Baggage (Optional):**
420
+
421
+ For type-safe baggage operations, use `defineBaggageSchema()`:
422
+
423
+ ```typescript
424
+ import { trace, defineBaggageSchema } from 'autotel';
425
+
426
+ type TenantBaggage = { tenantId: string; region?: string };
427
+ const tenantBaggage = defineBaggageSchema<TenantBaggage>('tenant');
428
+
429
+ export const handler = trace<TenantBaggage>((ctx) => async () => {
430
+ // Type-safe get
431
+ const tenant = tenantBaggage.get(ctx);
432
+ if (tenant?.tenantId) {
433
+ console.log('Tenant:', tenant.tenantId);
434
+ }
435
+
436
+ // Type-safe set with proper scoping
437
+ return await tenantBaggage.with(ctx, { tenantId: 't1' }, async () => {
438
+ // Baggage is available here and in child spans
439
+ });
440
+ });
441
+ ```
442
+
443
+ **Automatic Baggage → Span Attributes:**
444
+
445
+ Enable `baggage: true` in `init()` to automatically copy all baggage entries to span attributes, making them visible in trace UIs without manual `ctx.setAttribute()` calls:
446
+
447
+ ```typescript
448
+ import { init, trace, withBaggage } from 'autotel';
449
+
450
+ init({
451
+ service: 'my-app',
452
+ baggage: true, // Auto-copy baggage to span attributes
453
+ });
454
+
455
+ export const processOrder = trace((ctx) => async (order: Order) => {
456
+ return await withBaggage({
457
+ baggage: {
458
+ 'tenant.id': order.tenantId,
459
+ 'user.id': order.userId,
460
+ },
461
+ fn: async () => {
462
+ // Span automatically has baggage.tenant.id and baggage.user.id attributes!
463
+ // No need for: ctx.setAttribute('tenant.id', ctx.getBaggage('tenant.id'))
464
+ await chargeCustomer(order);
465
+ },
466
+ });
467
+ });
468
+ ```
469
+
470
+ **Custom prefix:**
471
+
472
+ ```typescript
473
+ init({
474
+ service: 'my-app',
475
+ baggage: 'ctx', // Creates ctx.tenant.id, ctx.user.id
476
+ // Or use '' for no prefix: tenant.id, user.id
477
+ });
478
+ ```
479
+
480
+ **Extracting Baggage from Incoming Requests:**
481
+
482
+ ```typescript
483
+ import { extractTraceContext, trace, context } from 'autotel';
484
+
485
+ // In Express middleware
486
+ app.use((req, res, next) => {
487
+ const extractedContext = extractTraceContext(req.headers);
488
+ context.with(extractedContext, () => {
489
+ next();
490
+ });
491
+ });
492
+ ```
493
+
494
+ **Key Points:**
495
+
496
+ - Typed baggage is completely optional - existing untyped baggage code continues to work without changes
497
+ - `baggage: true` in `init()` eliminates manual attribute setting for baggage
498
+ - Baggage values are strings (convert numbers/objects before setting)
499
+ - Never put PII in baggage - it propagates in HTTP headers across services!
500
+
501
+ ### Reusable Middleware Helpers
502
+
503
+ - `withTracing(options)` – create a preconfigured wrapper (service name, default attributes, skip rules).
504
+ - `instrument(object, options)` – batch-wrap entire modules while skipping helpers or private functions.
505
+
506
+ ```typescript
507
+ import { withTracing, instrument } from 'autotel';
508
+
509
+ const traceFn = withTracing({ serviceName: 'user' });
510
+
511
+ export const create = traceFn((ctx) => async (payload) => {
512
+ /* ... */
513
+ });
514
+ export const update = traceFn((ctx) => async (id, payload) => {
515
+ /* ... */
516
+ });
517
+
518
+ export const repository = instrument(
519
+ {
520
+ createUser: async () => {
521
+ /* ... */
522
+ },
523
+ updateUser: async () => {
524
+ /* ... */
525
+ },
526
+ _internal: async () => {
527
+ /* skipped */
528
+ },
529
+ },
530
+ { serviceName: 'repository', skip: ['_internal'] },
531
+ );
532
+ ```
533
+
534
+ ### Decorators (TypeScript 5+)
535
+
536
+ Prefer classes or NestJS-style services? Use the `@Trace` decorator.
537
+
538
+ ```typescript
539
+ import { Trace } from 'autotel/decorators';
540
+
541
+ class OrderService {
542
+ @Trace('order.create', { withMetrics: true })
543
+ async createOrder(data: OrderInput) {
544
+ return db.orders.create(data);
545
+ }
546
+
547
+ // No arguments → method name becomes the span name
548
+ @Trace()
549
+ async processPayment(orderId: string) {
550
+ return charge(orderId);
551
+ }
552
+
553
+ @Trace()
554
+ async refund(orderId: string) {
555
+ const ctx = (this as any).ctx;
556
+ ctx.setAttribute('order.id', orderId);
557
+ return refund(orderId);
558
+ }
559
+ }
560
+ ```
561
+
562
+ Decorators are optional, everything also works in plain functions.
563
+
564
+ ### Database Instrumentation
565
+
566
+ Turn on query tracing in one line.
567
+
568
+ ```typescript
569
+ import { instrumentDatabase } from 'autotel/db';
570
+
571
+ const db = drizzle(pool);
572
+
573
+ instrumentDatabase(db, {
574
+ dbSystem: 'postgresql',
575
+ dbName: 'myapp',
576
+ });
577
+
578
+ await db.select().from(users); // queries emit spans automatically
579
+ ```
580
+
581
+ ## Business Metrics & Product Events
582
+
583
+ Autotel treats metrics and events as first-class citizens so engineers and product teams share the same context.
584
+
585
+ ### OpenTelemetry Metrics (Metric class + helpers)
586
+
587
+ ```typescript
588
+ import { Metric, createHistogram } from 'autotel';
589
+
590
+ const metrics = new Metric('checkout');
591
+ const revenue = createHistogram('checkout.revenue');
592
+
593
+ export const processOrder = trace((ctx) => async (order) => {
594
+ metrics.trackEvent('order.completed', {
595
+ orderId: order.id,
596
+ amount: order.total,
597
+ });
598
+ metrics.trackValue('revenue', order.total, { currency: order.currency });
599
+ revenue.record(order.total, { currency: order.currency });
600
+ });
601
+ ```
602
+
603
+ - Emits OpenTelemetry counters/histograms via the OTLP endpoint configured in `init()`.
604
+ - Infrastructure metrics are enabled by default in **every** environment.
605
+
606
+ ### Product Events (PostHog, Mixpanel, Amplitude, …)
607
+
608
+ Track user behavior, conversion funnels, and business outcomes alongside your OpenTelemetry traces.
609
+
610
+ **Recommended: Configure subscribers in `init()`, use global `track()` function:**
611
+
612
+ ```typescript
613
+ import { init, track, trace } from 'autotel';
614
+ import { PostHogSubscriber } from 'autotel-subscribers/posthog';
615
+
616
+ init({
617
+ service: 'checkout',
618
+ subscribers: [new PostHogSubscriber({ apiKey: process.env.POSTHOG_KEY! })],
619
+ });
620
+
621
+ export const signup = trace('user.signup', async (user) => {
622
+ // All events use subscribers from init() automatically
623
+ track('user.signup', { userId: user.id, plan: user.plan });
624
+ track.funnelStep('checkout', 'completed', { cartValue: user.cartTotal });
625
+ track.value('lifetimeValue', user.cartTotal, { currency: 'USD' });
626
+ track.outcome('user.signup', 'success', { cohort: user.cohort });
627
+ });
628
+ ```
629
+
630
+ **Event instance (inherits subscribers from `init()`):**
631
+
632
+ ```typescript
633
+ import { Event } from 'autotel/event';
634
+
635
+ // Uses subscribers configured in init() - no need to pass them again
636
+ const events = new Event('checkout');
637
+
638
+ events.trackEvent('order.completed', { amount: 99.99 });
639
+ events.trackFunnelStep('checkout', 'started', { cartValue: 99.99 });
640
+ ```
641
+
642
+ **Override subscribers for specific Event instance:**
643
+
644
+ ```typescript
645
+ import { Event } from 'autotel/event';
646
+ import { MixpanelSubscriber } from 'autotel-subscribers/mixpanel';
647
+
648
+ // Override: use different subscribers for this instance (multi-tenant, A/B testing, etc.)
649
+ const marketingEvents = new Event('marketing', {
650
+ subscribers: [new MixpanelSubscriber({ token: process.env.MIXPANEL_TOKEN! })],
651
+ });
652
+
653
+ marketingEvents.trackEvent('campaign.viewed', { campaignId: '123' });
654
+ ```
655
+
656
+ **Subscriber Resolution:**
657
+
658
+ - If `subscribers` passed to Event constructor → uses those (instance override)
659
+ - If no `subscribers` passed → falls back to `init()` subscribers (global config)
660
+ - If neither configured → events logged only (graceful degradation)
661
+
662
+ Auto-enrichment adds `traceId`, `spanId`, `correlationId`, `operation.name`, `service.version`, and `deployment.environment` to every event payload without manual wiring.
663
+
664
+ ## Logging with Trace Context
665
+
666
+ **Bring your own logger** (Pino, Winston, Bunyan, etc.) and autotel automatically instruments it to:
667
+
668
+ - Inject trace context (`traceId`, `spanId`, `correlationId`) into every log record
669
+ - Record errors in the active OpenTelemetry span
670
+ - Bridge logs to the OpenTelemetry Logs API for OTLP export to Grafana, Datadog, etc.
671
+
672
+ ### Using Pino (recommended)
673
+
674
+ ```bash
675
+ npm install pino
676
+ ```
677
+
678
+ ```typescript
679
+ import pino from 'pino';
680
+ import { init, trace } from 'autotel';
681
+
682
+ const logger = pino({
683
+ level: process.env.LOG_LEVEL || 'info',
684
+ });
685
+
686
+ init({ service: 'user-service', logger });
687
+
688
+ export const createUser = trace(async (data: UserData) => {
689
+ logger.info({ userId: data.id }, 'Creating user');
690
+ try {
691
+ const user = await db.users.create(data);
692
+ logger.info({ userId: user.id }, 'User created');
693
+ return user;
694
+ } catch (error) {
695
+ logger.error({ err: error, userId: data.id }, 'Create failed');
696
+ throw error;
697
+ }
698
+ });
699
+ ```
700
+
701
+ ### Using Winston
702
+
703
+ ```bash
704
+ npm install winston
705
+ ```
706
+
707
+ ```typescript
708
+ import winston from 'winston';
709
+ import { init } from 'autotel';
710
+
711
+ const logger = winston.createLogger({
712
+ level: 'info',
713
+ format: winston.format.json(),
714
+ transports: [new winston.transports.Console()],
715
+ });
716
+
717
+ init({ service: 'user-service', logger });
718
+ ```
719
+
720
+ ### Using Bunyan (or other loggers)
721
+
722
+ Autotel auto-detects Pino and Winston. For other loggers like Bunyan, manually add the instrumentation:
723
+
724
+ ```bash
725
+ npm install bunyan @opentelemetry/instrumentation-bunyan
726
+ ```
727
+
728
+ ```typescript
729
+ import bunyan from 'bunyan';
730
+ import { init } from 'autotel';
731
+ import { BunyanInstrumentation } from '@opentelemetry/instrumentation-bunyan';
732
+
733
+ const logger = bunyan.createLogger({ name: 'user-service' });
734
+
735
+ init({
736
+ service: 'user-service',
737
+ logger,
738
+ instrumentations: [new BunyanInstrumentation()], // Manual instrumentation
739
+ });
740
+ ```
741
+
742
+ **Can't find your logger?** Check [OpenTelemetry JS Contrib](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages) for available instrumentations, or [open an issue](https://github.com/jagreehal/autotel/issues) to request official support!
743
+
744
+ ### What you get automatically
745
+
746
+ - ✅ Logs include `traceId`, `spanId`, `correlationId` for correlation with traces
747
+ - ✅ Errors are automatically recorded in the active span
748
+ - ✅ Logs export via OTLP to your observability backend (Grafana, Datadog, etc.)
749
+ - ✅ Zero configuration for Pino/Winston - just pass your logger to `init()`
750
+
751
+ ## Auto Instrumentation & Advanced Configuration
752
+
753
+ - `integrations` – Enable OpenTelemetry auto-instrumentations (HTTP, Express, Fastify, Prisma, Pino…). Requires `@opentelemetry/auto-instrumentations-node`.
754
+ - `instrumentations` – Provide manual instrumentation instances, e.g., `new HttpInstrumentation()`.
755
+ - `resource` / `resourceAttributes` – Declare cluster/region/tenant metadata once and it flows everywhere.
756
+ - `spanProcessor`, `metricReader`, `logRecordProcessors` – Plug in any OpenTelemetry exporter or your in-house pipeline.
757
+ - `otlpHeaders` – Attach vendor auth headers when using the built-in OTLP HTTP exporters.
758
+ - `sdkFactory` – Receive the Autotel defaults and return a fully customized `NodeSDK` for the rare cases you need complete control.
759
+
760
+ ```typescript
761
+ import { init } from 'autotel';
762
+ import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
763
+
764
+ init({
765
+ service: 'checkout',
766
+ environment: 'production',
767
+ resourceAttributes: {
768
+ 'cloud.region': 'us-east-1',
769
+ 'deployment.environment': 'production',
770
+ },
771
+ integrations: ['http', 'express', 'pino'],
772
+ instrumentations: [new HttpInstrumentation()],
773
+ otlpHeaders: 'Authorization=Basic ...',
774
+ subscribers: [new PostHogSubscriber({ apiKey: 'phc_xxx' })],
775
+ });
776
+ ```
777
+
778
+ ### ⚠️ Integrations vs. Manual Instrumentations
779
+
780
+ When using both `integrations` and `instrumentations`, manual instrumentations always take precedence. If you need custom configs (like `requireParentSpan: false` for standalone scripts), use **one or the other**:
781
+
782
+ #### Option A: Auto-instrumentations only (all defaults)
783
+
784
+ ```typescript
785
+ init({
786
+ service: 'my-app',
787
+ integrations: true, // All libraries with default configs
788
+ });
789
+ ```
790
+
791
+ #### Option B: Manual instrumentations with custom configs
792
+
793
+ ```typescript
794
+ import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb';
795
+ import { MongooseInstrumentation } from '@opentelemetry/instrumentation-mongoose';
796
+
797
+ init({
798
+ service: 'my-app',
799
+ integrations: false, // Must be false to avoid conflicts
800
+ instrumentations: [
801
+ new MongoDBInstrumentation({
802
+ requireParentSpan: false, // Custom config for scripts/cron jobs
803
+ }),
804
+ new MongooseInstrumentation({
805
+ requireParentSpan: false,
806
+ }),
807
+ ],
808
+ });
809
+ ```
810
+
811
+ #### Option C: Mix auto + manual (best of both)
812
+
813
+ ```typescript
814
+ import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb';
815
+
816
+ init({
817
+ service: 'my-app',
818
+ integrations: ['http', 'express'], // Auto for most libraries
819
+ instrumentations: [
820
+ // Manual config only for libraries that need custom settings
821
+ new MongoDBInstrumentation({
822
+ requireParentSpan: false,
823
+ }),
824
+ ],
825
+ });
826
+ ```
827
+
828
+ **Why `requireParentSpan` matters:** Many instrumentations default to `requireParentSpan: true`, which prevents spans from being created in standalone scripts, cron jobs, or background workers without an active parent span. Set it to `false` for these use cases.
829
+
830
+ ### ⚠️ Auto-Instrumentation Setup Requirements
831
+
832
+ OpenTelemetry's auto-instrumentation packages require special setup depending on your module system:
833
+
834
+ **CommonJS (Simpler - Recommended)**
835
+
836
+ No special flags required. Just use `--require`:
837
+
838
+ ```json
839
+ // package.json
840
+ {
841
+ "type": "commonjs" // or remove "type" field
842
+ }
843
+ ```
844
+
845
+ ```bash
846
+ node --require ./instrumentation.js src/server.js
847
+ ```
848
+
849
+ **ESM (Requires Experimental Loader Hook)**
850
+
851
+ If you need ESM, use the `--experimental-loader` flag:
852
+
853
+ ```bash
854
+ NODE_OPTIONS="--import ./instrumentation.mjs --experimental-loader=@opentelemetry/instrumentation/hook.mjs" node src/server.js
855
+ ```
856
+
857
+ For tsx users:
858
+
859
+ ```bash
860
+ NODE_OPTIONS="--experimental-loader=@opentelemetry/instrumentation/hook.mjs --import ./instrumentation.ts" tsx src/server.ts
861
+ ```
862
+
863
+ **Note:** The loader hook is an OpenTelemetry upstream requirement for ESM, not an autotel limitation. Autotel itself works identically in both ESM and CJS. See [OpenTelemetry ESM docs](https://opentelemetry.io/docs/languages/js/getting-started/nodejs/#esm-support) for details.
864
+
865
+ ## Operational Safety & Runtime Controls
866
+
867
+ - **Adaptive sampling** – 10% baseline, 100% for errors/slow spans by default (override via `sampler`).
868
+ - **Rate limiting & circuit breakers** – Prevent telemetry storms when backends misbehave.
869
+ - **Validation** – Configurable attribute/event name lengths, maximum counts, and nesting depth.
870
+ - **Sensitive data redaction** – Passwords, tokens, API keys, and any custom regex you provide are automatically masked before export.
871
+ - **Auto-flush** – Events buffers drain when root spans end (disable with `autoFlushEvents: false`).
872
+ - **Runtime flags** – Toggle metrics or swap endpoints via env vars without code edits.
873
+
874
+ ```bash
875
+ # Disable metrics without touching code (metrics are ON by default)
876
+ AUTOTELEMETRY_METRICS=off node server.js
877
+
878
+ # Point at a different collector
879
+ OTLP_ENDPOINT=https://otel.mycompany.com node server.js
880
+ ```
881
+
882
+ ## Configuration Reference
883
+
884
+ ```typescript
885
+ init({
886
+ service: string; // required
887
+ subscribers?: EventSubscriber[];
888
+ endpoint?: string;
889
+ protocol?: 'http' | 'grpc'; // OTLP protocol (default: 'http')
890
+ metrics?: boolean | 'auto';
891
+ sampler?: Sampler;
892
+ version?: string;
893
+ environment?: string;
894
+ baggage?: boolean | string; // Auto-copy baggage to span attributes
895
+ autoFlushEvents?: boolean; // Auto-flush events (default: true)
896
+ autoFlush?: boolean; // Auto-flush spans (default: false)
897
+ integrations?: string[] | boolean | Record<string, { enabled?: boolean }>;
898
+ instrumentations?: NodeSDKConfiguration['instrumentations'];
899
+ spanProcessor?: SpanProcessor;
900
+ metricReader?: MetricReader;
901
+ logRecordProcessors?: LogRecordProcessor[];
902
+ resource?: Resource;
903
+ resourceAttributes?: Record<string, string>;
904
+ otlpHeaders?: Record<string, string> | string;
905
+ sdkFactory?: (defaults: NodeSDK) => NodeSDK;
906
+ validation?: Partial<ValidationConfig>;
907
+ logger?: Logger; // created via createLogger() or bring your own
908
+ openllmetry?: {
909
+ enabled: boolean;
910
+ options?: Record<string, unknown>; // Passed to @traceloop/node-server-sdk
911
+ };
912
+ });
913
+ ```
914
+
915
+ **Event Subscribers:**
916
+
917
+ Configure event subscribers globally to send product events to PostHog, Mixpanel, Amplitude, etc.:
918
+
919
+ ```typescript
920
+ import { init } from 'autotel';
921
+ import { PostHogSubscriber } from 'autotel-subscribers/posthog';
922
+
923
+ init({
924
+ service: 'my-app',
925
+ subscribers: [new PostHogSubscriber({ apiKey: process.env.POSTHOG_KEY! })],
926
+ });
927
+ ```
928
+
929
+ Event instances automatically inherit these subscribers unless you explicitly override them. See [Product Events](#product-events-posthog-mixpanel-amplitude-) for details.
930
+
931
+ **Baggage Configuration:**
932
+
933
+ Enable automatic copying of baggage entries to span attributes:
934
+
935
+ ```typescript
936
+ init({
937
+ service: 'my-app',
938
+ baggage: true, // Copies baggage to span attributes with 'baggage.' prefix
939
+ });
940
+
941
+ // With custom prefix
942
+ init({
943
+ service: 'my-app',
944
+ baggage: 'ctx', // Copies with 'ctx.' prefix → ctx.tenant.id
945
+ });
946
+
947
+ // No prefix
948
+ init({
949
+ service: 'my-app',
950
+ baggage: '', // Copies directly → tenant.id
951
+ });
952
+ ```
953
+
954
+ This eliminates the need to manually call `ctx.setAttribute()` for baggage values. See [Baggage (Context Propagation)](#baggage-context-propagation) for usage examples.
955
+
956
+ **Protocol Configuration:**
957
+
958
+ Use the `protocol` parameter to switch between HTTP/protobuf (default) and gRPC:
959
+
960
+ ```typescript
961
+ // HTTP (default) - uses port 4318
962
+ init({
963
+ service: 'my-app',
964
+ protocol: 'http', // or omit (defaults to http)
965
+ endpoint: 'http://localhost:4318',
966
+ });
967
+
968
+ // gRPC - uses port 4317, better performance
969
+ init({
970
+ service: 'my-app',
971
+ protocol: 'grpc',
972
+ endpoint: 'localhost:4317',
973
+ });
974
+ ```
975
+
976
+ **Vendor Backend Configurations:**
977
+
978
+ For simplified setup with popular observability platforms, see [`autotel-backends`](../autotel-backends):
979
+
980
+ ```bash
981
+ npm install autotel-backends
982
+ ```
983
+
984
+ ```typescript
985
+ import { init } from 'autotel';
986
+ import { createDatadogConfig } from 'autotel-backends/datadog';
987
+ import { createHoneycombConfig } from 'autotel-backends/honeycomb';
988
+
989
+ // Datadog
990
+ init(
991
+ createDatadogConfig({
992
+ apiKey: process.env.DATADOG_API_KEY!,
993
+ service: 'my-app',
994
+ environment: 'production',
995
+ }),
996
+ );
997
+
998
+ // Honeycomb (automatically uses gRPC)
999
+ init(
1000
+ createHoneycombConfig({
1001
+ apiKey: process.env.HONEYCOMB_API_KEY!,
1002
+ service: 'my-app',
1003
+ environment: 'production',
1004
+ dataset: 'production', // optional, for classic accounts
1005
+ }),
1006
+ );
1007
+ ```
1008
+
1009
+ **Environment Variables:**
1010
+
1011
+ Autotel supports standard OpenTelemetry environment variables for zero-code configuration across environments:
1012
+
1013
+ ```bash
1014
+ # Service configuration
1015
+ export OTEL_SERVICE_NAME=my-app
1016
+
1017
+ # OTLP collector endpoint
1018
+ export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
1019
+
1020
+ # Protocol: 'http' or 'grpc' (default: 'http')
1021
+ export OTEL_EXPORTER_OTLP_PROTOCOL=http
1022
+
1023
+ # Authentication headers (comma-separated key=value pairs)
1024
+ export OTEL_EXPORTER_OTLP_HEADERS=x-honeycomb-team=YOUR_API_KEY
1025
+
1026
+ # Resource attributes (comma-separated key=value pairs)
1027
+ export OTEL_RESOURCE_ATTRIBUTES=service.version=1.2.3,deployment.environment=production,team=backend
1028
+ ```
1029
+
1030
+ **Configuration Precedence:** Explicit `init()` config > env vars > defaults
1031
+
1032
+ **Example: Honeycomb with env vars**
1033
+
1034
+ ```bash
1035
+ export OTEL_SERVICE_NAME=my-app
1036
+ export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io
1037
+ export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
1038
+ export OTEL_EXPORTER_OTLP_HEADERS=x-honeycomb-team=YOUR_API_KEY
1039
+ export OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production
1040
+ ```
1041
+
1042
+ **Example: Datadog with env vars**
1043
+
1044
+ ```bash
1045
+ export OTEL_SERVICE_NAME=my-app
1046
+ export OTEL_EXPORTER_OTLP_ENDPOINT=https://http-intake.logs.datadoghq.com
1047
+ export OTEL_EXPORTER_OTLP_HEADERS=DD-API-KEY=YOUR_API_KEY
1048
+ export OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production
1049
+ ```
1050
+
1051
+ See `packages/autotel/.env.example` for a complete template.
1052
+
1053
+ Validation tuning example:
1054
+
1055
+ ```typescript
1056
+ init({
1057
+ service: 'checkout',
1058
+ validation: {
1059
+ sensitivePatterns: [/password/i, /secret/i, /creditCard/i],
1060
+ maxAttributeValueLength: 5_000,
1061
+ maxAttributeCount: 100,
1062
+ maxNestingDepth: 5,
1063
+ },
1064
+ });
1065
+ ```
1066
+
1067
+ ## Building Custom Instrumentation
1068
+
1069
+ Autotel is designed as an **enabler** - it provides composable primitives that let you instrument anything in your codebase. Here's how to use the building blocks to create custom instrumentation for queues, cron jobs, and other patterns.
1070
+
1071
+ ### Instrumenting Queue Consumers
1072
+
1073
+ ```typescript
1074
+ import { trace, span, track } from 'autotel';
1075
+
1076
+ // Wrap your consumer handler with trace()
1077
+ export const processMessage = trace(async function processMessage(
1078
+ message: Message,
1079
+ ) {
1080
+ // Use span() to break down processing stages
1081
+ await span({ name: 'parse.message' }, async (ctx) => {
1082
+ ctx.setAttribute('message.id', message.id);
1083
+ ctx.setAttribute('message.type', message.type);
1084
+ return parseMessage(message);
1085
+ });
1086
+
1087
+ await span({ name: 'validate.message' }, async () => {
1088
+ return validateMessage(message);
1089
+ });
1090
+
1091
+ await span({ name: 'process.business.logic' }, async () => {
1092
+ return handleMessage(message);
1093
+ });
1094
+
1095
+ // Track events
1096
+ track('message.processed', {
1097
+ messageType: message.type,
1098
+ processingTime: Date.now() - message.timestamp,
1099
+ });
1100
+ });
1101
+
1102
+ // Use in your queue consumer
1103
+ consumer.on('message', async (msg) => {
1104
+ await processMessage(msg);
1105
+ });
1106
+ ```
1107
+
1108
+ ### Instrumenting Scheduled Jobs / Cron
1109
+
1110
+ ```typescript
1111
+ import { trace, getMetrics } from 'autotel';
1112
+
1113
+ export const dailyReportJob = trace(async function dailyReportJob() {
1114
+ const metrics = getMetrics();
1115
+ const startTime = Date.now();
1116
+
1117
+ try {
1118
+ const report = await generateReport();
1119
+
1120
+ // Record success metrics
1121
+ metrics.recordHistogram('job.duration', Date.now() - startTime, {
1122
+ job_name: 'daily_report',
1123
+ status: 'success',
1124
+ });
1125
+
1126
+ return report;
1127
+ } catch (error) {
1128
+ // Record failure metrics
1129
+ metrics.recordHistogram('job.duration', Date.now() - startTime, {
1130
+ job_name: 'daily_report',
1131
+ status: 'error',
1132
+ });
1133
+ throw error;
1134
+ }
1135
+ });
1136
+
1137
+ // Schedule with your preferred library
1138
+ cron.schedule('0 0 * * *', () => dailyReportJob());
1139
+ ```
1140
+
1141
+ ### Creating Custom Event Subscribers
1142
+
1143
+ Implement the `EventSubscriber` interface to send events to any events platform:
1144
+
1145
+ ```typescript
1146
+ import { type EventSubscriber, type EventAttributes } from 'autotel';
1147
+
1148
+ export class CustomEventSubscriber implements EventSubscriber {
1149
+ constructor(private config: { apiKey: string; endpoint: string }) {}
1150
+
1151
+ async track(
1152
+ eventName: string,
1153
+ attributes: EventAttributes,
1154
+ timestamp: Date,
1155
+ ): Promise<void> {
1156
+ await fetch(this.config.endpoint, {
1157
+ method: 'POST',
1158
+ headers: { 'X-API-Key': this.config.apiKey },
1159
+ body: JSON.stringify({
1160
+ event: eventName,
1161
+ properties: attributes,
1162
+ timestamp,
1163
+ }),
1164
+ });
1165
+ }
1166
+
1167
+ async identify(userId: string, traits: EventAttributes): Promise<void> {
1168
+ // Implement user identification
1169
+ }
1170
+
1171
+ async flush(): Promise<void> {
1172
+ // Implement flush if buffering
1173
+ }
1174
+ }
1175
+
1176
+ // Use it in init()
1177
+ init({
1178
+ service: 'my-app',
1179
+ subscribers: [new CustomEventSubscriber({ apiKey: '...', endpoint: '...' })],
1180
+ });
1181
+ ```
1182
+
1183
+ ### Low-Level Span Manipulation
1184
+
1185
+ For maximum control, use the `ctx` proxy or the ergonomic tracer helpers:
1186
+
1187
+ ```typescript
1188
+ import { ctx, getTracer, getActiveSpan, runWithSpan } from 'autotel';
1189
+
1190
+ export async function customWorkflow() {
1191
+ // Access current trace context anywhere (via AsyncLocalStorage)
1192
+ console.log('Current trace:', ctx.traceId);
1193
+ ctx.setAttribute('workflow.step', 'start');
1194
+
1195
+ // Or create custom spans with the tracer helpers
1196
+ const tracer = getTracer('my-custom-tracer');
1197
+ const span = tracer.startSpan('custom.operation');
1198
+
1199
+ try {
1200
+ // Your logic here
1201
+ span.setAttribute('custom.attribute', 'value');
1202
+ span.setStatus({ code: SpanStatusCode.OK });
1203
+ } finally {
1204
+ span.end();
1205
+ }
1206
+ }
1207
+
1208
+ // Add attributes and events to the currently active span
1209
+ export function enrichCurrentSpan(userId: string) {
1210
+ const span = getActiveSpan();
1211
+ if (span) {
1212
+ span.setAttribute('user.id', userId);
1213
+ span.addEvent('User identified', { userId, timestamp: Date.now() });
1214
+ }
1215
+ }
1216
+
1217
+ // Add events with attributes (e.g., queue operations)
1218
+ export async function processQueue() {
1219
+ const span = getActiveSpan();
1220
+ if (span) {
1221
+ span.addEvent('queue.wait', {
1222
+ queue_size: 42,
1223
+ queue_name: 'order-processing',
1224
+ });
1225
+ // Process queue...
1226
+ }
1227
+ }
1228
+
1229
+ // Run code with a specific span as active
1230
+ export async function backgroundJob() {
1231
+ const tracer = getTracer('background-processor');
1232
+ const span = tracer.startSpan('process.batch');
1233
+
1234
+ try {
1235
+ await runWithSpan(span, async () => {
1236
+ // Any spans created here will be children of 'process.batch'
1237
+ await processRecords();
1238
+ });
1239
+ span.setStatus({ code: SpanStatusCode.OK });
1240
+ } catch (error) {
1241
+ span.recordException(error);
1242
+ span.setStatus({ code: SpanStatusCode.ERROR });
1243
+ } finally {
1244
+ span.end();
1245
+ }
1246
+ }
1247
+ ```
1248
+
1249
+ **Available tracer helpers:**
1250
+
1251
+ - `getTracer(name, version?)` - Get a tracer for creating custom spans
1252
+ - `getActiveSpan()` - Get the currently active span
1253
+ - `getActiveContext()` - Get the current OpenTelemetry context
1254
+ - `runWithSpan(span, fn)` - Execute a function with a span set as active
1255
+
1256
+ > **Note:** For most use cases, prefer `trace()`, `span()`, or `instrument()` which handle span lifecycle automatically.
1257
+
1258
+ ### Custom Metrics
1259
+
1260
+ Create custom business metrics using the meter helpers:
1261
+
1262
+ ```typescript
1263
+ import { getMeter, createCounter, createHistogram } from 'autotel';
1264
+
1265
+ // Create custom metrics
1266
+ const requestCounter = createCounter('http.requests.total', {
1267
+ description: 'Total HTTP requests',
1268
+ });
1269
+
1270
+ const responseTimeHistogram = createHistogram('http.response.time', {
1271
+ description: 'HTTP response time in milliseconds',
1272
+ unit: 'ms',
1273
+ });
1274
+
1275
+ export async function handleRequest(req: Request) {
1276
+ const startTime = Date.now();
1277
+
1278
+ requestCounter.add(1, { method: req.method, path: req.path });
1279
+
1280
+ const response = await processRequest(req);
1281
+
1282
+ responseTimeHistogram.record(Date.now() - startTime, {
1283
+ method: req.method,
1284
+ status: response.status,
1285
+ });
1286
+
1287
+ return response;
1288
+ }
1289
+ ```
1290
+
1291
+ **Key Principle:** All these primitives work together - spans automatically capture context, metrics and events inherit trace IDs, and everything flows through the same configured exporters and adapters. Build what you need, when you need it.
1292
+
1293
+ ## Serverless & Short-lived Processes
1294
+
1295
+ For serverless environments (AWS Lambda, Vercel, Cloud Functions) and other short-lived processes, telemetry may not export before the process ends. Autotel provides two approaches:
1296
+
1297
+ ### Manual Flush (Recommended for Serverless)
1298
+
1299
+ Use the `flush()` function to force-export all telemetry before the function returns:
1300
+
1301
+ ```typescript
1302
+ import { init, flush } from 'autotel';
1303
+
1304
+ init({
1305
+ service: 'my-lambda',
1306
+ endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
1307
+ });
1308
+
1309
+ export const handler = async (event) => {
1310
+ // Your business logic here
1311
+ const result = await processEvent(event);
1312
+
1313
+ // Force-flush telemetry before returning
1314
+ await flush();
1315
+
1316
+ return result;
1317
+ };
1318
+ ```
1319
+
1320
+ The `flush()` function:
1321
+
1322
+ - Flushes events from the queue
1323
+ - Force-flushes OpenTelemetry spans to exporters
1324
+ - Includes timeout protection (default: 2000ms)
1325
+ - Safe to call multiple times
1326
+
1327
+ **Custom timeout:**
1328
+
1329
+ ```typescript
1330
+ await flush({ timeout: 5000 }); // 5 second timeout
1331
+ ```
1332
+
1333
+ ### Auto-Flush Spans (Opt-in)
1334
+
1335
+ Enable automatic span flushing on root span completion:
1336
+
1337
+ ```typescript
1338
+ init({
1339
+ service: 'my-lambda',
1340
+ autoFlushEvents: true, // enabled by default (events only)
1341
+ autoFlush: true, // flush spans on root completion
1342
+ });
1343
+
1344
+ export const handler = trace(async (event) => {
1345
+ // Auto-flushes when trace completes
1346
+ return await processEvent(event);
1347
+ });
1348
+ ```
1349
+
1350
+ **Trade-offs:**
1351
+
1352
+ - ✅ Zero boilerplate - no manual `flush()` needed
1353
+ - ✅ Guaranteed export before process ends (async functions only)
1354
+ - ⚠️ Adds ~50-200ms latency per request (network I/O)
1355
+ - ⚠️ Only needed for short-lived processes
1356
+ - ⚠️ Only applies to async traced functions (synchronous functions cannot await flush)
1357
+
1358
+ **When to use:**
1359
+
1360
+ - Use `autoFlush: true` for serverless functions where latency is acceptable
1361
+ - Use manual `flush()` for more control over when flushing occurs
1362
+ - Use neither for long-running services (batch export is more efficient)
1363
+
1364
+ ### Edge Runtimes (Cloudflare Workers, Vercel Edge)
1365
+
1366
+ For edge runtimes with different constraints, use the `autotel-edge` package instead:
1367
+
1368
+ ```typescript
1369
+ import { init } from 'autotel-edge';
1370
+ // Auto-flush built-in for edge environments
1371
+ ```
1372
+
1373
+ The `autotel-edge` package is optimized for edge runtimes with automatic flush behavior.
1374
+
1375
+ ## API Reference
1376
+
1377
+ - `init(config)` – Bootstraps the SDK (call once).
1378
+ - `trace(fn | name, fn)` – Wraps functions with spans and optional context access.
1379
+ - `span(options, fn)` – Creates nested spans for ad-hoc blocks.
1380
+ - `withTracing(options)` – Produces reusable wrappers with shared configuration.
1381
+ - `instrument(target, options)` – Batch-wraps an object of functions.
1382
+ - `Trace` decorator – Adds tracing to class methods (TypeScript 5+).
1383
+ - `instrumentDatabase(db, options)` – Adds automatic DB spans (Drizzle, etc.).
1384
+ - `Metric` class & helpers (`createHistogram`, etc.) – Emit OpenTelemetry metrics.
1385
+ - `Event` class & `track()` helper – Send product events/funnels/outcomes/values via subscribers.
1386
+ - `Logger` interface – Bring your own Pino/Winston logger; autotel auto-instruments it for trace context and OTLP export.
1387
+ - `PostHogSubscriber`, `MixpanelSubscriber`, … – Provided in `autotel-subscribers`; create your own by implementing the `EventSubscriber` interface.
1388
+
1389
+ Each API is type-safe, works in both ESM and CJS, and is designed to minimize boilerplate while staying close to OpenTelemetry primitives.
1390
+
1391
+ ## FAQ & Next Steps
1392
+
1393
+ - **Do I need to abandon my current tooling?** No. Autotel layers on top of OpenTelemetry and forwards to whatever you already use (Datadog, Grafana, Tempo, Honeycomb, etc.).
1394
+ - **Is this just for traces?** No. Spans, metrics, logs, and events all share the same context and exporters.
1395
+ - **Can I customize everything?** Yes. Override exporters, readers, resources, validation, or even the full NodeSDK via `sdkFactory`.
1396
+ - **Does it work in production?** Yes. Adaptive sampling, redaction, validation, rate limiting, and circuit breakers are enabled out of the box.
1397
+ - **What about frameworks?** Use decorators, `withTracing()`, or `instrument()` for NestJS, Fastify, Express, Next.js actions, queues, workers, anything in Node.js.
1398
+
1399
+ **Next steps:**
1400
+
1401
+ 1. `npm install autotel` and call `init()` at startup.
1402
+ 2. Wrap your critical paths with `trace()` (or `Trace` decorators if you prefer classes).
1403
+ 3. Point the OTLP endpoint at your favorite observability backend and optionally add events adapters.
1404
+ 4. Expand coverage with `instrumentDatabase()`, `withTracing()`, metrics, logging, and auto-instrumentations.
1405
+
1406
+ ## Troubleshooting & Debugging
1407
+
1408
+ ### Quick Debug Mode (Recommended)
1409
+
1410
+ The simplest way to see spans locally during development - perfect for progressive development:
1411
+
1412
+ ```typescript
1413
+ import { init } from 'autotel';
1414
+
1415
+ // Start with console-only (no backend needed)
1416
+ init({
1417
+ service: 'my-app',
1418
+ debug: true, // Outputs spans to console
1419
+ });
1420
+
1421
+ // Later: add endpoint to send to backend while keeping console output
1422
+ init({
1423
+ service: 'my-app',
1424
+ debug: true,
1425
+ endpoint: 'https://otlp.datadoghq.com', // Now sends to both console AND Datadog
1426
+ });
1427
+
1428
+ // Production: remove debug to send to backend only
1429
+ init({
1430
+ service: 'my-app',
1431
+ endpoint: 'https://otlp.datadoghq.com', // Backend only (clean production config)
1432
+ });
1433
+ ```
1434
+
1435
+ **How it Works:**
1436
+
1437
+ - **`debug: true`**: Print spans to console AND send to backend (if endpoint configured)
1438
+ - No endpoint = console-only (perfect for local development)
1439
+ - With endpoint = console + backend (verify before choosing provider)
1440
+ - **No debug flag**: Export to backend only (default production behavior)
1441
+
1442
+ **Environment Variable:**
1443
+
1444
+ ```bash
1445
+ # Enable debug mode
1446
+ AUTOLEMETRY_DEBUG=true node server.js
1447
+ # or
1448
+ AUTOLEMETRY_DEBUG=1 node server.js
1449
+
1450
+ # Disable debug mode
1451
+ AUTOLEMETRY_DEBUG=false node server.js
1452
+ ```
1453
+
1454
+ ### Manual Configuration (Advanced)
1455
+
1456
+ When developing or debugging your instrumentation, you may want more control over span export. Autotel supports manual exporter configuration:
1457
+
1458
+ #### ConsoleSpanExporter (Visual Debugging)
1459
+
1460
+ Use `ConsoleSpanExporter` to print all spans to the console in real-time. This is great for:
1461
+
1462
+ - Quick visual inspection during development
1463
+ - Seeing spans as they're created
1464
+ - Debugging span structure and attributes
1465
+ - Examples and demos
1466
+
1467
+ ```typescript
1468
+ import { init } from 'autotel';
1469
+ import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base';
1470
+
1471
+ init({
1472
+ service: 'my-app',
1473
+ spanExporter: new ConsoleSpanExporter(), // Prints spans to console
1474
+ });
1475
+ ```
1476
+
1477
+ ### InMemorySpanExporter (Testing & Assertions)
1478
+
1479
+ Use `InMemorySpanExporter` for programmatic access to spans in tests. This is ideal for:
1480
+
1481
+ - Writing test assertions on spans
1482
+ - Querying spans by name or attributes
1483
+ - Verifying instrumentation behavior
1484
+ - Automated testing
1485
+
1486
+ ```typescript
1487
+ import { init } from 'autotel';
1488
+ import {
1489
+ InMemorySpanExporter,
1490
+ SimpleSpanProcessor,
1491
+ } from '@opentelemetry/sdk-trace-base';
1492
+
1493
+ const exporter = new InMemorySpanExporter();
1494
+
1495
+ init({
1496
+ service: 'test',
1497
+ spanProcessor: new SimpleSpanProcessor(exporter),
1498
+ });
1499
+
1500
+ // After running your code...
1501
+ const spans = exporter.getFinishedSpans();
1502
+ expect(spans).toHaveLength(1);
1503
+ expect(spans[0]?.name).toBe('my.operation');
1504
+ ```
1505
+
1506
+ ### Using Both (Advanced)
1507
+
1508
+ For comprehensive debugging, use the `debug: true` option to combine console output with backend export. See the "Quick Debug Mode" section above for the recommended approach.
1509
+
1510
+ **Quick Reference:**
1511
+
1512
+ - **ConsoleSpanExporter**: See spans in console output (development/debugging)
1513
+ - **InMemorySpanExporter**: Query spans programmatically (testing/assertions)
1514
+
1515
+ ## Creating Custom Instrumentation
1516
+
1517
+ Autotel provides utilities that make it easy to instrument any library with OpenTelemetry tracing. Whether you need to instrument an internal tool, a database driver without official support, or any other library, autotel's helper functions handle the complexity for you.
1518
+
1519
+ ### Quick Start Template
1520
+
1521
+ Here's the minimal code to instrument any library:
1522
+
1523
+ ```typescript
1524
+ import { trace, SpanKind } from '@opentelemetry/api';
1525
+ import { runWithSpan, finalizeSpan } from 'autotel/trace-helpers';
1526
+
1527
+ const INSTRUMENTED_FLAG = Symbol('instrumented');
1528
+
1529
+ export function instrumentMyLibrary(client) {
1530
+ if (client[INSTRUMENTED_FLAG]) return client;
1531
+
1532
+ const tracer = trace.getTracer('my-library');
1533
+ const originalMethod = client.someMethod.bind(client);
1534
+
1535
+ client.someMethod = async function (...args) {
1536
+ const span = tracer.startSpan('operation.name', {
1537
+ kind: SpanKind.CLIENT,
1538
+ });
1539
+
1540
+ span.setAttribute('operation.param', args[0]);
1541
+
1542
+ try {
1543
+ const result = await runWithSpan(span, () => originalMethod(...args));
1544
+ finalizeSpan(span);
1545
+ return result;
1546
+ } catch (error) {
1547
+ finalizeSpan(span, error);
1548
+ throw error;
1549
+ }
1550
+ };
1551
+
1552
+ client[INSTRUMENTED_FLAG] = true;
1553
+ return client;
1554
+ }
1555
+ ```
1556
+
1557
+ ### Step-by-Step Tutorial: Instrumenting Axios
1558
+
1559
+ Let's walk through instrumenting the popular axios HTTP client:
1560
+
1561
+ ```typescript
1562
+ import { trace, SpanKind } from '@opentelemetry/api';
1563
+ import { runWithSpan, finalizeSpan } from 'autotel/trace-helpers';
1564
+ import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
1565
+
1566
+ // Step 1: Create instrumentation flag to prevent double-instrumentation
1567
+ const INSTRUMENTED_FLAG = Symbol('axiosInstrumented');
1568
+
1569
+ interface InstrumentedAxios {
1570
+ [INSTRUMENTED_FLAG]?: boolean;
1571
+ }
1572
+
1573
+ // Step 2: Define configuration for your instrumentation
1574
+ export interface InstrumentAxiosConfig {
1575
+ tracerName?: string;
1576
+ captureHeaders?: boolean;
1577
+ captureRequestBody?: boolean;
1578
+ captureResponseBody?: boolean;
1579
+ }
1580
+
1581
+ // Step 3: Create the instrumentation function
1582
+ export function instrumentAxios(
1583
+ axios: AxiosInstance,
1584
+ config?: InstrumentAxiosConfig,
1585
+ ): AxiosInstance {
1586
+ const instrumented = axios as AxiosInstance & InstrumentedAxios;
1587
+
1588
+ // Idempotent check
1589
+ if (instrumented[INSTRUMENTED_FLAG]) {
1590
+ return axios;
1591
+ }
1592
+
1593
+ const {
1594
+ tracerName = 'axios-http-client',
1595
+ captureHeaders = false,
1596
+ captureRequestBody = false,
1597
+ captureResponseBody = false,
1598
+ } = config ?? {};
1599
+
1600
+ // Step 4: Get tracer instance
1601
+ const tracer = trace.getTracer(tracerName);
1602
+
1603
+ // Step 5: Add request interceptor to start spans
1604
+ axios.interceptors.request.use((requestConfig: AxiosRequestConfig) => {
1605
+ const url = requestConfig.url || '';
1606
+ const method = requestConfig.method?.toUpperCase() || 'GET';
1607
+
1608
+ // Step 6: Start span with appropriate attributes
1609
+ const span = tracer.startSpan(`HTTP ${method}`, {
1610
+ kind: SpanKind.CLIENT,
1611
+ });
1612
+
1613
+ // Follow OpenTelemetry semantic conventions
1614
+ span.setAttribute('http.method', method);
1615
+ span.setAttribute('http.url', url);
1616
+
1617
+ if (captureHeaders && requestConfig.headers) {
1618
+ span.setAttribute(
1619
+ 'http.request.headers',
1620
+ JSON.stringify(requestConfig.headers),
1621
+ );
1622
+ }
1623
+
1624
+ if (captureRequestBody && requestConfig.data) {
1625
+ span.setAttribute(
1626
+ 'http.request.body',
1627
+ JSON.stringify(requestConfig.data),
1628
+ );
1629
+ }
1630
+
1631
+ // Store span in request config for response interceptor
1632
+ (requestConfig as any).__span = span;
1633
+
1634
+ return requestConfig;
1635
+ });
1636
+
1637
+ // Step 7: Add response interceptor to finalize spans
1638
+ axios.interceptors.response.use(
1639
+ (response: AxiosResponse) => {
1640
+ const span = (response.config as any).__span;
1641
+ if (span) {
1642
+ span.setAttribute('http.status_code', response.status);
1643
+
1644
+ if (captureResponseBody && response.data) {
1645
+ span.setAttribute(
1646
+ 'http.response.body',
1647
+ JSON.stringify(response.data),
1648
+ );
1649
+ }
1650
+
1651
+ // Step 8: Finalize span on success
1652
+ finalizeSpan(span);
1653
+ }
1654
+ return response;
1655
+ },
1656
+ (error) => {
1657
+ const span = error.config?.__span;
1658
+ if (span) {
1659
+ if (error.response) {
1660
+ span.setAttribute('http.status_code', error.response.status);
1661
+ }
1662
+ // Step 9: Finalize span on error (records exception)
1663
+ finalizeSpan(span, error);
1664
+ }
1665
+ return Promise.reject(error);
1666
+ },
1667
+ );
1668
+
1669
+ // Step 10: Mark as instrumented
1670
+ instrumented[INSTRUMENTED_FLAG] = true;
1671
+ return axios;
1672
+ }
1673
+
1674
+ // Usage:
1675
+ import axios from 'axios';
1676
+ import { init } from 'autotel';
1677
+
1678
+ init({ service: 'my-api' });
1679
+
1680
+ const client = axios.create({ baseURL: 'https://api.example.com' });
1681
+ instrumentAxios(client, { captureHeaders: true });
1682
+
1683
+ // All requests are now traced
1684
+ await client.get('/users');
1685
+ ```
1686
+
1687
+ ### Best Practices
1688
+
1689
+ #### 1. Idempotent Instrumentation
1690
+
1691
+ Always use symbols or flags to prevent double-instrumentation:
1692
+
1693
+ ```typescript
1694
+ const INSTRUMENTED_FLAG = Symbol('instrumented');
1695
+
1696
+ export function instrument(client) {
1697
+ if (client[INSTRUMENTED_FLAG]) {
1698
+ return client; // Already instrumented
1699
+ }
1700
+
1701
+ // ... instrumentation code ...
1702
+
1703
+ client[INSTRUMENTED_FLAG] = true;
1704
+ return client;
1705
+ }
1706
+ ```
1707
+
1708
+ #### 2. Error Handling
1709
+
1710
+ Always use try/catch with `finalizeSpan` to ensure spans are properly closed:
1711
+
1712
+ ```typescript
1713
+ try {
1714
+ const result = await runWithSpan(span, () => operation());
1715
+ finalizeSpan(span); // Sets OK status and ends span
1716
+ return result;
1717
+ } catch (error) {
1718
+ finalizeSpan(span, error); // Records exception, sets ERROR status, ends span
1719
+ throw error;
1720
+ }
1721
+ ```
1722
+
1723
+ #### 3. Security - Don't Capture Sensitive Data
1724
+
1725
+ Be extremely careful about what you capture in spans:
1726
+
1727
+ ```typescript
1728
+ export interface Config {
1729
+ captureQueryText?: boolean; // Default: false for security
1730
+ captureFilters?: boolean; // Default: false for security
1731
+ captureHeaders?: boolean; // Default: false for security
1732
+ }
1733
+
1734
+ function instrument(client, config) {
1735
+ // Only capture if explicitly enabled
1736
+ if (config.captureQueryText) {
1737
+ span.setAttribute('db.statement', sanitize(query));
1738
+ }
1739
+ }
1740
+ ```
1741
+
1742
+ #### 4. Follow OpenTelemetry Semantic Conventions
1743
+
1744
+ Use standard attribute names from [OpenTelemetry Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/):
1745
+
1746
+ ```typescript
1747
+ // ✅ Good - Standard conventions
1748
+ span.setAttribute('http.method', 'GET');
1749
+ span.setAttribute('http.status_code', 200);
1750
+ span.setAttribute('db.system', 'postgresql');
1751
+ span.setAttribute('db.operation', 'SELECT');
1752
+ span.setAttribute('messaging.system', 'kafka');
1753
+
1754
+ // ❌ Bad - Custom names
1755
+ span.setAttribute('method', 'GET');
1756
+ span.setAttribute('status', 200);
1757
+ span.setAttribute('database', 'postgres');
1758
+ ```
1759
+
1760
+ #### 5. Choose the Right SpanKind
1761
+
1762
+ ```typescript
1763
+ import { SpanKind } from '@opentelemetry/api';
1764
+
1765
+ // CLIENT: Outgoing requests, database calls, API calls
1766
+ tracer.startSpan('http.request', { kind: SpanKind.CLIENT });
1767
+
1768
+ // SERVER: Incoming requests (usually auto-instrumented)
1769
+ tracer.startSpan('http.server', { kind: SpanKind.SERVER });
1770
+
1771
+ // INTERNAL: Internal operations, business logic
1772
+ tracer.startSpan('process.data', { kind: SpanKind.INTERNAL });
1773
+
1774
+ // PRODUCER: Publishing messages to queues
1775
+ tracer.startSpan('kafka.produce', { kind: SpanKind.PRODUCER });
1776
+
1777
+ // CONSUMER: Consuming messages from queues
1778
+ tracer.startSpan('kafka.consume', { kind: SpanKind.CONSUMER });
1779
+ ```
1780
+
1781
+ #### 6. TypeScript Type Safety
1782
+
1783
+ Make your instrumentation type-safe:
1784
+
1785
+ ```typescript
1786
+ import type { MyLibrary } from 'my-library';
1787
+
1788
+ interface InstrumentedClient {
1789
+ __instrumented?: boolean;
1790
+ }
1791
+
1792
+ export function instrument<T extends MyLibrary>(client: T, config?: Config): T {
1793
+ const instrumented = client as T & InstrumentedClient;
1794
+ // ... instrumentation ...
1795
+ return client;
1796
+ }
1797
+ ```
1798
+
1799
+ ### Available Utilities
1800
+
1801
+ Autotel provides these utilities for custom instrumentation:
1802
+
1803
+ #### From `autotel/trace-helpers`
1804
+
1805
+ ```typescript
1806
+ import {
1807
+ getTracer, // Get tracer instance
1808
+ runWithSpan, // Execute function with span as active context
1809
+ finalizeSpan, // Set status and end span with error handling
1810
+ getActiveSpan, // Get currently active span
1811
+ getTraceContext, // Get trace IDs for correlation
1812
+ enrichWithTraceContext, // Add trace context to objects
1813
+ getActiveContext, // Get current OpenTelemetry context
1814
+ } from 'autotel/trace-helpers';
1815
+
1816
+ // Get a tracer
1817
+ const tracer = getTracer('my-service', '1.0.0');
1818
+
1819
+ // Start a span
1820
+ const span = tracer.startSpan('operation.name');
1821
+
1822
+ // Run code with span as active context
1823
+ const result = await runWithSpan(span, async () => {
1824
+ // Any spans created here will be children of 'span'
1825
+ return await doWork();
1826
+ });
1827
+
1828
+ // Finalize span (OK status if no error, ERROR status if error provided)
1829
+ finalizeSpan(span); // Success
1830
+ finalizeSpan(span, error); // Error
1831
+
1832
+ // Get current active span (to add attributes)
1833
+ const currentSpan = getActiveSpan();
1834
+ if (currentSpan) {
1835
+ currentSpan.setAttribute('user.id', userId);
1836
+ }
1837
+
1838
+ // Get trace context for logging correlation
1839
+ const context = getTraceContext();
1840
+ // { traceId: '...', spanId: '...', correlationId: '...' }
1841
+
1842
+ // Enrich log objects with trace context
1843
+ logger.info(
1844
+ enrichWithTraceContext({
1845
+ message: 'User logged in',
1846
+ userId: '123',
1847
+ }),
1848
+ );
1849
+ // Logs: { message: '...', userId: '123', traceId: '...', spanId: '...' }
1850
+ ```
1851
+
1852
+ #### From `@opentelemetry/api`
1853
+
1854
+ ```typescript
1855
+ import {
1856
+ trace, // Access to tracer provider
1857
+ context, // Context management (advanced)
1858
+ SpanKind, // CLIENT, SERVER, INTERNAL, PRODUCER, CONSUMER
1859
+ SpanStatusCode, // OK, ERROR, UNSET
1860
+ type Span, // Span interface
1861
+ type Tracer, // Tracer interface
1862
+ } from '@opentelemetry/api';
1863
+
1864
+ // Span methods
1865
+ span.setAttribute(key, value); // Add single attribute
1866
+ span.setAttributes({ key: value }); // Add multiple attributes
1867
+ span.addEvent('cache.hit'); // Add event (name only)
1868
+ span.addEvent('queue.wait', { queue_size: 42 }); // Add event with attributes
1869
+ span.recordException(error); // Record exception
1870
+ span.setStatus({ code: SpanStatusCode.ERROR }); // Set status
1871
+ span.end(); // End span
1872
+ ```
1873
+
1874
+ #### Semantic Conventions (Optional)
1875
+
1876
+ For database instrumentation, you can reuse constants from `autotel-plugins`:
1877
+
1878
+ ```typescript
1879
+ import {
1880
+ SEMATTRS_DB_SYSTEM,
1881
+ SEMATTRS_DB_OPERATION,
1882
+ SEMATTRS_DB_NAME,
1883
+ SEMATTRS_DB_STATEMENT,
1884
+ SEMATTRS_NET_PEER_NAME,
1885
+ SEMATTRS_NET_PEER_PORT,
1886
+ } from 'autotel-plugins/common/constants';
1887
+
1888
+ span.setAttribute(SEMATTRS_DB_SYSTEM, 'postgresql');
1889
+ span.setAttribute(SEMATTRS_DB_OPERATION, 'SELECT');
1890
+ ```
1891
+
1892
+ ### Real-World Examples
1893
+
1894
+ **Complete instrumentation template:**
1895
+ See [`INSTRUMENTATION_TEMPLATE.ts`](./INSTRUMENTATION_TEMPLATE.ts) for a comprehensive, commented template you can copy and customize.
1896
+
1897
+ **Production example:**
1898
+ Check [`autotel-plugins/drizzle`](../autotel-plugins/src/drizzle/index.ts) for a real-world instrumentation of Drizzle ORM showing:
1899
+
1900
+ - Idempotent instrumentation
1901
+ - Multiple instrumentation levels (client, database, session)
1902
+ - Configuration options
1903
+ - Security considerations (query text capture)
1904
+ - Full TypeScript support
1905
+
1906
+ ### When to Create Custom Instrumentation
1907
+
1908
+ ✅ **Create custom instrumentation when:**
1909
+
1910
+ - No official `@opentelemetry/instrumentation-*` package exists
1911
+ - You're instrumenting internal tools or proprietary libraries
1912
+ - You need more control over captured data
1913
+ - You want simpler configuration than official packages
1914
+
1915
+ ❌ **Use official packages when available:**
1916
+
1917
+ - MongoDB: `@opentelemetry/instrumentation-mongodb`
1918
+ - Mongoose: `@opentelemetry/instrumentation-mongoose`
1919
+ - PostgreSQL: `@opentelemetry/instrumentation-pg`
1920
+ - MySQL: `@opentelemetry/instrumentation-mysql2`
1921
+ - Redis: `@opentelemetry/instrumentation-redis`
1922
+ - See all: [opentelemetry-js-contrib](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node)
1923
+
1924
+ ### Using Official Instrumentation
1925
+
1926
+ To use official OpenTelemetry instrumentation with autotel:
1927
+
1928
+ ```typescript
1929
+ import { init } from 'autotel';
1930
+ import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb';
1931
+ import { RedisInstrumentation } from '@opentelemetry/instrumentation-redis';
1932
+
1933
+ init({
1934
+ service: 'my-service',
1935
+ instrumentations: [
1936
+ new MongoDBInstrumentation({
1937
+ enhancedDatabaseReporting: true,
1938
+ }),
1939
+ new RedisInstrumentation(),
1940
+ ],
1941
+ });
1942
+
1943
+ // MongoDB and Redis operations are now automatically traced
1944
+ ```
1945
+
1946
+ Happy observing!