@photon-ai/otel 0.1.2 → 1.0.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.
package/README.md CHANGED
@@ -5,7 +5,7 @@ A DX-focused OpenTelemetry wrapper for **Bun** and **Node.js**.
5
5
  Vanilla OTel works, but the setup is verbose, the logger plumbing is awkward, and PII scrubbing is on you. `@photon-ai/otel` wraps the OTLP/HTTP stack into a few well-named functions:
6
6
 
7
7
  - **`setupOtel()`** — idempotent one-call bootstrap for traces + logs. Honors all standard `OTEL_EXPORTER_OTLP_*` env vars.
8
- - **`createLogger(module)`** — structured logger that writes to both the OTel logger provider and `console`, with automatic trace correlation and exception capture.
8
+ - **`createLogger(module)`** — structured logger that writes to both the OTel logger provider and `console`, with automatic trace correlation and exception capture. Every level (`debug`/`info`/`warn`/`error`) accepts `attrs` **and** an `error`, and is gated by a configurable `LOG_LEVEL`.
9
9
  - **`withSpan(name, attrs?, fn)`** — wrap any sync or async function in a span; errors are recorded and PII in the error message is scrubbed before being attached to span status.
10
10
  - **`sanitizeEmail` / `sanitizePhone` / `sanitizeErrorMessage`** — PII helpers you can reuse anywhere.
11
11
 
@@ -47,6 +47,8 @@ If `OTEL_EXPORTER_OTLP_ENDPOINT` (or the `endpoint` option) is unset, `setupOtel
47
47
  | `setupOtel(options): OtelHandle` | Boots OTLP/HTTP traces + logs. Idempotent. Returns `{ shutdown(): Promise<void> }`. |
48
48
  | `isOtelActive(): boolean` | Returns `true` if `setupOtel` has already run in this process. |
49
49
  | `createLogger(module): PhotonLogger` | Returns `{ info, warn, error, debug }`. Each call emits to OTel + `console`, correlates to active span. |
50
+ | `setLogLevel(level): void` | Set the minimum level emitted (`debug`/`info`/`warn`/`error`/`silent`). `LOG_LEVEL` env still wins. |
51
+ | `getLogLevel(): LogLevel` | Current effective level after env / override / default resolution. |
50
52
  | `withSpan(name, fn)` | Wraps `fn` (sync or async) in a span. Records exceptions and scrubs PII in error messages. |
51
53
  | `withSpan(name, attrs, fn)` | Same as above but attaches `attrs` to the span. |
52
54
  | `sanitizeEmail(input)` | Masks an email: `foo.bar@example.com` → `fo***@e***.com`. |
@@ -57,13 +59,42 @@ If `OTEL_EXPORTER_OTLP_ENDPOINT` (or the `endpoint` option) is unset, `setupOtel
57
59
  ### Logger signatures
58
60
 
59
61
  ```ts
60
- log.info(message, attrs?);
61
- log.warn(message, attrs?);
62
- log.error(message, attrs?, error?); // only error() accepts an exception
63
- log.debug(message, attrs?);
62
+ log.debug(message, attrs?, error?);
63
+ log.info(message, attrs?, error?);
64
+ log.warn(message, attrs?, error?); // attach the exception that caused a retry
65
+ log.error(message, attrs?, error?);
64
66
  ```
65
67
 
66
- `attrs` is `Record<string, string | number | boolean | undefined>`. `undefined` values are dropped.
68
+ Every level takes the same `(message, attrs?, error?)` shape attach an exception to a
69
+ `warn`/`info`/`debug`, not just `error`. `attrs` is
70
+ `Record<string, string | number | boolean | undefined>`; `undefined` values are dropped.
71
+
72
+ An `Error` is recorded as `exception.type` / `exception.message` / `exception.stacktrace`
73
+ on the OTLP record (per the OTel exception semantic convention); a non-`Error` throw is
74
+ coerced so at least `exception.message` is preserved.
75
+
76
+ Each call also prints a single human-readable console line — `[module] LEVEL message
77
+ { ...attrs }` plus the raw error (so the runtime renders the full stack) — routed to
78
+ `console.debug` / `console.info` / `console.warn` / `console.error` by severity. Both
79
+ sinks share one level gate.
80
+
81
+ ### Log level
82
+
83
+ Logs below the active level are dropped from **both** OTLP and the console. The level is
84
+ resolved fresh on every call, so changes take effect immediately:
85
+
86
+ 1. `LOG_LEVEL` env var (`debug` | `info` | `warn` | `error` | `silent`) — wins if set.
87
+ 2. `setLogLevel(level)` or `setupOtel({ logLevel })`.
88
+ 3. Default: `debug` in development (`DEPLOYMENT_ENV` unset or `development`), `info` otherwise.
89
+
90
+ ```ts
91
+ import { setLogLevel } from "@photon-ai/otel";
92
+
93
+ setLogLevel("warn"); // debug + info now suppressed everywhere
94
+ // or set LOG_LEVEL=warn in the environment, which overrides the call above
95
+ ```
96
+
97
+ `"silent"` suppresses everything, including errors.
67
98
 
68
99
  ## Configuration
69
100
 
@@ -75,7 +106,8 @@ Standard OpenTelemetry env vars always take precedence over `SetupOtelOptions`:
75
106
  | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | Full traces URL (overrides the base for traces). |
76
107
  | `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | Full logs URL (overrides the base for logs). |
77
108
  | `OTEL_EXPORTER_OTLP_HEADERS` | `key=value,key=value` headers; merged with `options.headers` (env wins). |
78
- | `DEPLOYMENT_ENV` | Attached as `deployment.environment` resource attribute. Defaults to `development`. |
109
+ | `DEPLOYMENT_ENV` | Attached as `deployment.environment` resource attribute. Defaults to `development`. Also drives the default log level. |
110
+ | `LOG_LEVEL` | Minimum log level: `debug` \| `info` \| `warn` \| `error` \| `silent`. Overrides `setLogLevel()` / `setupOtel({ logLevel })`. |
79
111
 
80
112
  ## Running on Node vs Bun
81
113
 
package/dist/index.d.ts CHANGED
@@ -1,9 +1,21 @@
1
1
  type LogAttrs = Record<string, string | number | boolean | undefined>;
2
+ /**
3
+ * Minimum severity that gets emitted (to both the OTLP record and the console).
4
+ * `"silent"` suppresses everything, including errors.
5
+ */
6
+ type LogLevel = "debug" | "info" | "warn" | "error" | "silent";
7
+ /**
8
+ * Programmatically set the minimum log level. Takes effect immediately for
9
+ * subsequent logs. `LOG_LEVEL` env var still wins if set.
10
+ */
11
+ declare function setLogLevel(level: LogLevel): void;
12
+ /** Current effective log level, after env / override / default resolution. */
13
+ declare function getLogLevel(): LogLevel;
2
14
  interface PhotonLogger {
3
- debug(message: string, attrs?: LogAttrs): void;
15
+ debug(message: string, attrs?: LogAttrs, error?: unknown): void;
4
16
  error(message: string, attrs?: LogAttrs, error?: unknown): void;
5
- info(message: string, attrs?: LogAttrs): void;
6
- warn(message: string, attrs?: LogAttrs): void;
17
+ info(message: string, attrs?: LogAttrs, error?: unknown): void;
18
+ warn(message: string, attrs?: LogAttrs, error?: unknown): void;
7
19
  }
8
20
  declare function createLogger(module: string): PhotonLogger;
9
21
 
@@ -41,6 +53,12 @@ interface SetupOtelOptions {
41
53
  * conflicts.
42
54
  */
43
55
  headers?: Record<string, string>;
56
+ /**
57
+ * Minimum log level emitted by `createLogger()` (to both OTLP and console).
58
+ * The `LOG_LEVEL` env var still takes precedence. Defaults to `debug` in
59
+ * development and `info` otherwise.
60
+ */
61
+ logLevel?: LogLevel;
44
62
  /**
45
63
  * Extra resource attributes attached to every span/log alongside
46
64
  * `service.name` / `service.version`.
@@ -74,4 +92,4 @@ declare const PHOTON_OTEL_VERSION = "0.1.0";
74
92
  declare function withSpan<T>(name: string, fn: () => Promise<T> | T): Promise<T>;
75
93
  declare function withSpan<T>(name: string, attrs: LogAttrs, fn: () => Promise<T> | T): Promise<T>;
76
94
 
77
- export { type LogAttrs, type OtelHandle, PHOTON_OTEL_VERSION, type PhotonLogger, type SetupOtelOptions, createLogger, isOtelActive, sanitizeEmail, sanitizeErrorMessage, sanitizePhone, setupOtel, withSpan };
95
+ export { type LogAttrs, type LogLevel, type OtelHandle, PHOTON_OTEL_VERSION, type PhotonLogger, type SetupOtelOptions, createLogger, getLogLevel, isOtelActive, sanitizeEmail, sanitizeErrorMessage, sanitizePhone, setLogLevel, setupOtel, withSpan };
package/dist/index.js CHANGED
@@ -6,6 +6,37 @@ import { logs, SeverityNumber } from "@opentelemetry/api-logs";
6
6
  var PHOTON_OTEL_VERSION = "0.1.0";
7
7
 
8
8
  // src/logger.ts
9
+ var LEVEL_SEVERITY = {
10
+ debug: SeverityNumber.DEBUG,
11
+ // 5
12
+ info: SeverityNumber.INFO,
13
+ // 9
14
+ warn: SeverityNumber.WARN,
15
+ // 13
16
+ error: SeverityNumber.ERROR,
17
+ // 17
18
+ silent: Number.POSITIVE_INFINITY
19
+ };
20
+ var levelOverride;
21
+ function envLevel() {
22
+ const raw = process.env.LOG_LEVEL?.toLowerCase();
23
+ if (raw && raw in LEVEL_SEVERITY) {
24
+ return raw;
25
+ }
26
+ return;
27
+ }
28
+ function defaultLevel() {
29
+ return (process.env.DEPLOYMENT_ENV ?? "development") === "development" ? "debug" : "info";
30
+ }
31
+ function resolveLevel() {
32
+ return envLevel() ?? levelOverride ?? defaultLevel();
33
+ }
34
+ function setLogLevel(level) {
35
+ levelOverride = level;
36
+ }
37
+ function getLogLevel() {
38
+ return resolveLevel();
39
+ }
9
40
  var scopedLogger;
10
41
  function getLogger() {
11
42
  if (!scopedLogger) {
@@ -25,10 +56,26 @@ function filterUndefined(attrs) {
25
56
  }
26
57
  return out;
27
58
  }
59
+ function consoleFor(severityNumber) {
60
+ if (severityNumber >= SeverityNumber.ERROR) {
61
+ return console.error;
62
+ }
63
+ if (severityNumber >= SeverityNumber.WARN) {
64
+ return console.warn;
65
+ }
66
+ if (severityNumber >= SeverityNumber.INFO) {
67
+ return console.info;
68
+ }
69
+ return console.debug;
70
+ }
28
71
  function emit(severityNumber, severityText, module, message, attrs, error) {
72
+ if (severityNumber < LEVEL_SEVERITY[resolveLevel()]) {
73
+ return;
74
+ }
75
+ const userAttrs = filterUndefined(attrs);
29
76
  const attributes = {
30
77
  "log.module": module,
31
- ...filterUndefined(attrs)
78
+ ...userAttrs
32
79
  };
33
80
  if (error instanceof Error) {
34
81
  attributes["exception.type"] = error.name;
@@ -36,6 +83,9 @@ function emit(severityNumber, severityText, module, message, attrs, error) {
36
83
  if (error.stack) {
37
84
  attributes["exception.stacktrace"] = error.stack;
38
85
  }
86
+ } else if (error !== void 0) {
87
+ attributes["exception.type"] = typeof error;
88
+ attributes["exception.message"] = String(error);
39
89
  }
40
90
  getLogger().emit({
41
91
  severityNumber,
@@ -44,19 +94,21 @@ function emit(severityNumber, severityText, module, message, attrs, error) {
44
94
  attributes,
45
95
  context: otelContext.active()
46
96
  });
47
- const prefix = `[${module}]`;
48
- if (severityNumber >= SeverityNumber.ERROR) {
49
- console.error(prefix, message, ...error ? [error] : []);
50
- } else {
51
- console.log(prefix, message);
97
+ const extras = [];
98
+ if (Object.keys(userAttrs).length > 0) {
99
+ extras.push(userAttrs);
52
100
  }
101
+ if (error !== void 0) {
102
+ extras.push(error);
103
+ }
104
+ consoleFor(severityNumber)(`[${module}]`, severityText, message, ...extras);
53
105
  }
54
106
  function createLogger(module) {
55
107
  return {
56
- info: (message, attrs) => emit(SeverityNumber.INFO, "INFO", module, message, attrs),
57
- warn: (message, attrs) => emit(SeverityNumber.WARN, "WARN", module, message, attrs),
58
- error: (message, attrs, error) => emit(SeverityNumber.ERROR, "ERROR", module, message, attrs, error),
59
- debug: (message, attrs) => emit(SeverityNumber.DEBUG, "DEBUG", module, message, attrs)
108
+ debug: (message, attrs, error) => emit(SeverityNumber.DEBUG, "DEBUG", module, message, attrs, error),
109
+ info: (message, attrs, error) => emit(SeverityNumber.INFO, "INFO", module, message, attrs, error),
110
+ warn: (message, attrs, error) => emit(SeverityNumber.WARN, "WARN", module, message, attrs, error),
111
+ error: (message, attrs, error) => emit(SeverityNumber.ERROR, "ERROR", module, message, attrs, error)
60
112
  };
61
113
  }
62
114
 
@@ -149,6 +201,9 @@ function setupOtel(options) {
149
201
  if (activeHandle) {
150
202
  return activeHandle;
151
203
  }
204
+ if (options.logLevel) {
205
+ setLogLevel(options.logLevel);
206
+ }
152
207
  const tracesEndpoint = resolveTracesEndpoint(options.endpoint);
153
208
  const logsEndpoint = resolveLogsEndpoint(options.endpoint);
154
209
  const mergedHeaders = {
@@ -257,10 +312,12 @@ function withSpan(name, attrsOrFn, maybeFn) {
257
312
  export {
258
313
  PHOTON_OTEL_VERSION,
259
314
  createLogger,
315
+ getLogLevel,
260
316
  isOtelActive,
261
317
  sanitizeEmail,
262
318
  sanitizeErrorMessage,
263
319
  sanitizePhone,
320
+ setLogLevel,
264
321
  setupOtel,
265
322
  withSpan
266
323
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/logger.ts","../src/version.ts","../src/sanitize.ts","../src/setup.ts","../src/with-span.ts"],"sourcesContent":["import { context as otelContext } from \"@opentelemetry/api\";\nimport { type Logger, logs, SeverityNumber } from \"@opentelemetry/api-logs\";\nimport { PHOTON_OTEL_VERSION } from \"./version\";\n\nexport type LogAttrs = Record<string, string | number | boolean | undefined>;\n\nlet scopedLogger: Logger | undefined;\n\nfunction getLogger(): Logger {\n if (!scopedLogger) {\n scopedLogger = logs.getLogger(\"@photon-ai/otel\", PHOTON_OTEL_VERSION);\n }\n return scopedLogger;\n}\n\nfunction filterUndefined(\n attrs?: LogAttrs\n): Record<string, string | number | boolean> {\n if (!attrs) {\n return {};\n }\n const out: Record<string, string | number | boolean> = {};\n for (const [k, v] of Object.entries(attrs)) {\n if (v !== undefined) {\n out[k] = v;\n }\n }\n return out;\n}\n\nfunction emit(\n severityNumber: SeverityNumber,\n severityText: string,\n module: string,\n message: string,\n attrs?: LogAttrs,\n error?: unknown\n): void {\n const attributes: Record<string, string | number | boolean> = {\n \"log.module\": module,\n ...filterUndefined(attrs),\n };\n\n if (error instanceof Error) {\n attributes[\"exception.type\"] = error.name;\n attributes[\"exception.message\"] = error.message;\n if (error.stack) {\n attributes[\"exception.stacktrace\"] = error.stack;\n }\n }\n\n getLogger().emit({\n severityNumber,\n severityText,\n body: message,\n attributes,\n context: otelContext.active(),\n });\n\n const prefix = `[${module}]`;\n if (severityNumber >= SeverityNumber.ERROR) {\n console.error(prefix, message, ...(error ? [error] : []));\n } else {\n console.log(prefix, message);\n }\n}\n\nexport interface PhotonLogger {\n debug(message: string, attrs?: LogAttrs): void;\n error(message: string, attrs?: LogAttrs, error?: unknown): void;\n info(message: string, attrs?: LogAttrs): void;\n warn(message: string, attrs?: LogAttrs): void;\n}\n\nexport function createLogger(module: string): PhotonLogger {\n return {\n info: (message, attrs) =>\n emit(SeverityNumber.INFO, \"INFO\", module, message, attrs),\n warn: (message, attrs) =>\n emit(SeverityNumber.WARN, \"WARN\", module, message, attrs),\n error: (message, attrs, error) =>\n emit(SeverityNumber.ERROR, \"ERROR\", module, message, attrs, error),\n debug: (message, attrs) =>\n emit(SeverityNumber.DEBUG, \"DEBUG\", module, message, attrs),\n };\n}\n","export const PHOTON_OTEL_VERSION = \"0.1.0\";\n","// E.164-ish phone match: optional `+`, 7–15 digits with optional separators.\nconst PHONE_PATTERN = /\\+?\\d[\\d\\s()\\-.]{6,18}\\d/g;\nconst EMAIL_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/g;\n\n/**\n * Mask a phone number, keeping the leading `+` (if any) plus the first 3 digits\n * and the last 4 digits visible. Example: `+13315553374` -> `+133xxxxx3374`.\n *\n * Inputs that don't have enough digits to safely mask are returned as\n * `xxxx` to avoid leaking the entire short value.\n */\nexport function sanitizePhone(input: string): string {\n const hasPlus = input.startsWith(\"+\");\n const digits = input.replace(/\\D/g, \"\");\n if (digits.length < 8) {\n return hasPlus ? \"+xxxx\" : \"xxxx\";\n }\n const head = digits.slice(0, 3);\n const tail = digits.slice(-4);\n const middleLength = digits.length - head.length - tail.length;\n return `${hasPlus ? \"+\" : \"\"}${head}${\"x\".repeat(middleLength)}${tail}`;\n}\n\n/**\n * Mask an email address, keeping the first 2 chars of the local part, the\n * first char of the domain, and the TLD. Example:\n * `foo.bar@example.com` -> `fo***@e***.com`.\n */\nexport function sanitizeEmail(input: string): string {\n const atIndex = input.lastIndexOf(\"@\");\n if (atIndex < 1) {\n return \"***\";\n }\n const local = input.slice(0, atIndex);\n const domain = input.slice(atIndex + 1);\n const dotIndex = domain.lastIndexOf(\".\");\n if (dotIndex < 1) {\n return \"***\";\n }\n const localHead = local.slice(0, 2);\n const domainHead = domain.slice(0, 1);\n const tld = domain.slice(dotIndex);\n return `${localHead}***@${domainHead}***${tld}`;\n}\n\n/**\n * Replace every phone number and email address inside a free-form string with\n * its sanitized form. Used to scrub `Error.message` values before attaching\n * them to span status.\n */\nexport function sanitizeErrorMessage(input: string): string {\n return input\n .replace(EMAIL_PATTERN, (match) => sanitizeEmail(match))\n .replace(PHONE_PATTERN, (match) => sanitizePhone(match));\n}\n","import { context, trace } from \"@opentelemetry/api\";\nimport { logs } from \"@opentelemetry/api-logs\";\nimport { AsyncLocalStorageContextManager } from \"@opentelemetry/context-async-hooks\";\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-http\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport {\n BatchLogRecordProcessor,\n LoggerProvider,\n} from \"@opentelemetry/sdk-logs\";\nimport {\n BasicTracerProvider,\n BatchSpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\n\nexport interface SetupOtelOptions {\n /**\n * Default OTLP/HTTP base endpoint (e.g. `https://otel.example.com`). The\n * `/v1/traces` and `/v1/logs` paths are appended automatically. Standard\n * `OTEL_EXPORTER_OTLP_*` env vars always take precedence.\n */\n endpoint?: string;\n /**\n * Default OTLP headers (e.g. `{ Authorization: \"Basic ...\" }`). Merged with\n * any headers parsed from `OTEL_EXPORTER_OTLP_HEADERS`; env values win on\n * conflicts.\n */\n headers?: Record<string, string>;\n /**\n * Extra resource attributes attached to every span/log alongside\n * `service.name` / `service.version`.\n */\n resourceAttributes?: Record<string, string | number | boolean>;\n serviceName: string;\n serviceVersion?: string;\n}\n\nexport interface OtelHandle {\n shutdown(): Promise<void>;\n}\n\nlet activeHandle: OtelHandle | undefined;\n\nconst TRAILING_SLASH = /\\/$/;\n\nfunction parseEnvHeaders(raw: string | undefined): Record<string, string> {\n if (!raw) {\n return {};\n }\n const out: Record<string, string> = {};\n for (const pair of raw.split(\",\")) {\n const eq = pair.indexOf(\"=\");\n if (eq <= 0) {\n continue;\n }\n const key = pair.slice(0, eq).trim();\n const value = pair.slice(eq + 1).trim();\n if (key) {\n out[key] = value;\n }\n }\n return out;\n}\n\nfunction resolveTracesEndpoint(base: string | undefined): string | undefined {\n const traces = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT;\n if (traces) {\n return traces;\n }\n const generic = process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? base;\n return generic\n ? `${generic.replace(TRAILING_SLASH, \"\")}/v1/traces`\n : undefined;\n}\n\nfunction resolveLogsEndpoint(base: string | undefined): string | undefined {\n const logsEndpoint = process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT;\n if (logsEndpoint) {\n return logsEndpoint;\n }\n const generic = process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? base;\n return generic ? `${generic.replace(TRAILING_SLASH, \"\")}/v1/logs` : undefined;\n}\n\n/**\n * Boot an OTLP/HTTP-based OpenTelemetry pipeline (traces + logs).\n *\n * Idempotent: calling twice in the same process is a no-op on the second\n * call, so libraries can safely invoke this without clobbering an app-level\n * OTel setup that ran earlier.\n *\n * Standard `OTEL_EXPORTER_OTLP_*` env vars override the `endpoint` and\n * `headers` arguments — this matches the OpenTelemetry SDK config spec.\n */\nexport function setupOtel(options: SetupOtelOptions): OtelHandle {\n if (activeHandle) {\n return activeHandle;\n }\n\n const tracesEndpoint = resolveTracesEndpoint(options.endpoint);\n const logsEndpoint = resolveLogsEndpoint(options.endpoint);\n const mergedHeaders = {\n ...options.headers,\n ...parseEnvHeaders(process.env.OTEL_EXPORTER_OTLP_HEADERS),\n };\n const hasHeaders = Object.keys(mergedHeaders).length > 0;\n\n const resource = resourceFromAttributes({\n \"service.name\": options.serviceName,\n ...(options.serviceVersion\n ? { \"service.version\": options.serviceVersion }\n : {}),\n \"deployment.environment\": process.env.DEPLOYMENT_ENV ?? \"development\",\n ...options.resourceAttributes,\n });\n\n context.setGlobalContextManager(new AsyncLocalStorageContextManager());\n\n const traceProcessors = tracesEndpoint\n ? [\n new BatchSpanProcessor(\n new OTLPTraceExporter({\n url: tracesEndpoint,\n headers: hasHeaders ? mergedHeaders : undefined,\n })\n ),\n ]\n : [];\n\n const tracerProvider = new BasicTracerProvider({\n resource,\n spanProcessors: traceProcessors,\n });\n trace.setGlobalTracerProvider(tracerProvider);\n\n const logProcessors = logsEndpoint\n ? [\n new BatchLogRecordProcessor(\n new OTLPLogExporter({\n url: logsEndpoint,\n headers: hasHeaders ? mergedHeaders : undefined,\n })\n ),\n ]\n : [];\n\n const loggerProvider = new LoggerProvider({\n resource,\n processors: logProcessors,\n });\n logs.setGlobalLoggerProvider(loggerProvider);\n\n const handle: OtelHandle = {\n async shutdown() {\n await Promise.allSettled([\n tracerProvider.shutdown(),\n loggerProvider.shutdown(),\n ]);\n activeHandle = undefined;\n },\n };\n\n activeHandle = handle;\n return handle;\n}\n\n/**\n * Read-only accessor for tests / debug paths that need to know whether\n * `setupOtel` has already run in this process.\n */\nexport function isOtelActive(): boolean {\n return activeHandle !== undefined;\n}\n","import {\n type Attributes,\n SpanStatusCode,\n type Tracer,\n trace,\n} from \"@opentelemetry/api\";\nimport type { LogAttrs } from \"./logger\";\nimport { sanitizeErrorMessage } from \"./sanitize\";\nimport { PHOTON_OTEL_VERSION } from \"./version\";\n\nlet scopedTracer: Tracer | undefined;\n\nfunction getTracer(): Tracer {\n if (!scopedTracer) {\n scopedTracer = trace.getTracer(\"@photon-ai/otel\", PHOTON_OTEL_VERSION);\n }\n return scopedTracer;\n}\n\nfunction toAttributes(attrs: LogAttrs): Attributes {\n const out: Attributes = {};\n for (const [k, v] of Object.entries(attrs)) {\n if (v !== undefined) {\n out[k] = v;\n }\n }\n return out;\n}\n\nexport function withSpan<T>(name: string, fn: () => Promise<T> | T): Promise<T>;\nexport function withSpan<T>(\n name: string,\n attrs: LogAttrs,\n fn: () => Promise<T> | T\n): Promise<T>;\nexport function withSpan<T>(\n name: string,\n attrsOrFn: LogAttrs | (() => Promise<T> | T),\n maybeFn?: () => Promise<T> | T\n): Promise<T> {\n const fn = typeof attrsOrFn === \"function\" ? attrsOrFn : maybeFn;\n if (!fn) {\n throw new Error(\"withSpan: function argument is required\");\n }\n const attrs = typeof attrsOrFn === \"function\" ? undefined : attrsOrFn;\n\n return getTracer().startActiveSpan(name, async (span) => {\n if (attrs) {\n span.setAttributes(toAttributes(attrs));\n }\n try {\n const result = await fn();\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (err) {\n span.recordException(err as Error);\n const errorObj = err instanceof Error ? err : undefined;\n span.setAttribute(\"error.type\", errorObj?.constructor.name ?? typeof err);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: errorObj\n ? sanitizeErrorMessage(errorObj.message)\n : sanitizeErrorMessage(String(err)),\n });\n throw err;\n } finally {\n span.end();\n }\n });\n}\n"],"mappings":"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","names":["logs","trace","trace"]}
1
+ {"version":3,"sources":["../src/logger.ts","../src/version.ts","../src/sanitize.ts","../src/setup.ts","../src/with-span.ts"],"sourcesContent":["import { context as otelContext } from \"@opentelemetry/api\";\nimport { type Logger, logs, SeverityNumber } from \"@opentelemetry/api-logs\";\nimport { PHOTON_OTEL_VERSION } from \"./version\";\n\nexport type LogAttrs = Record<string, string | number | boolean | undefined>;\n\n/**\n * Minimum severity that gets emitted (to both the OTLP record and the console).\n * `\"silent\"` suppresses everything, including errors.\n */\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\" | \"silent\";\n\nconst LEVEL_SEVERITY: Record<LogLevel, number> = {\n debug: SeverityNumber.DEBUG, // 5\n info: SeverityNumber.INFO, // 9\n warn: SeverityNumber.WARN, // 13\n error: SeverityNumber.ERROR, // 17\n silent: Number.POSITIVE_INFINITY,\n};\n\nlet levelOverride: LogLevel | undefined;\n\nfunction envLevel(): LogLevel | undefined {\n const raw = process.env.LOG_LEVEL?.toLowerCase();\n if (raw && raw in LEVEL_SEVERITY) {\n return raw as LogLevel;\n }\n return;\n}\n\nfunction defaultLevel(): LogLevel {\n return (process.env.DEPLOYMENT_ENV ?? \"development\") === \"development\"\n ? \"debug\"\n : \"info\";\n}\n\n/**\n * Resolve the active level fresh on each call so that `LOG_LEVEL` changes and\n * `setLogLevel()` both take effect immediately. Resolution order (env wins, to\n * match the rest of the package's config story):\n * 1. `LOG_LEVEL` env var\n * 2. `setLogLevel()` / `setupOtel({ logLevel })`\n * 3. environment-driven default (`debug` in development, `info` otherwise)\n */\nfunction resolveLevel(): LogLevel {\n return envLevel() ?? levelOverride ?? defaultLevel();\n}\n\n/**\n * Programmatically set the minimum log level. Takes effect immediately for\n * subsequent logs. `LOG_LEVEL` env var still wins if set.\n */\nexport function setLogLevel(level: LogLevel): void {\n levelOverride = level;\n}\n\n/** Current effective log level, after env / override / default resolution. */\nexport function getLogLevel(): LogLevel {\n return resolveLevel();\n}\n\nlet scopedLogger: Logger | undefined;\n\nfunction getLogger(): Logger {\n if (!scopedLogger) {\n scopedLogger = logs.getLogger(\"@photon-ai/otel\", PHOTON_OTEL_VERSION);\n }\n return scopedLogger;\n}\n\nfunction filterUndefined(\n attrs?: LogAttrs\n): Record<string, string | number | boolean> {\n if (!attrs) {\n return {};\n }\n const out: Record<string, string | number | boolean> = {};\n for (const [k, v] of Object.entries(attrs)) {\n if (v !== undefined) {\n out[k] = v;\n }\n }\n return out;\n}\n\nfunction consoleFor(\n severityNumber: SeverityNumber\n): (...args: unknown[]) => void {\n if (severityNumber >= SeverityNumber.ERROR) {\n return console.error;\n }\n if (severityNumber >= SeverityNumber.WARN) {\n return console.warn;\n }\n if (severityNumber >= SeverityNumber.INFO) {\n return console.info;\n }\n return console.debug;\n}\n\nfunction emit(\n severityNumber: SeverityNumber,\n severityText: string,\n module: string,\n message: string,\n attrs?: LogAttrs,\n error?: unknown\n): void {\n // Single gate: drop sub-threshold logs before they reach OTLP or the console.\n if (severityNumber < LEVEL_SEVERITY[resolveLevel()]) {\n return;\n }\n\n const userAttrs = filterUndefined(attrs);\n const attributes: Record<string, string | number | boolean> = {\n \"log.module\": module,\n ...userAttrs,\n };\n\n if (error instanceof Error) {\n attributes[\"exception.type\"] = error.name;\n attributes[\"exception.message\"] = error.message;\n if (error.stack) {\n attributes[\"exception.stacktrace\"] = error.stack;\n }\n } else if (error !== undefined) {\n // Don't silently drop non-Error throws (strings, plain objects, etc.).\n attributes[\"exception.type\"] = typeof error;\n attributes[\"exception.message\"] = String(error);\n }\n\n getLogger().emit({\n severityNumber,\n severityText,\n body: message,\n attributes,\n context: otelContext.active(),\n });\n\n // Console: `[module] LEVEL message { ...attrs }` plus the raw error so the\n // runtime renders the full stack and pretty-prints the attribute bag.\n const extras: unknown[] = [];\n if (Object.keys(userAttrs).length > 0) {\n extras.push(userAttrs);\n }\n if (error !== undefined) {\n extras.push(error);\n }\n consoleFor(severityNumber)(`[${module}]`, severityText, message, ...extras);\n}\n\nexport interface PhotonLogger {\n debug(message: string, attrs?: LogAttrs, error?: unknown): void;\n error(message: string, attrs?: LogAttrs, error?: unknown): void;\n info(message: string, attrs?: LogAttrs, error?: unknown): void;\n warn(message: string, attrs?: LogAttrs, error?: unknown): void;\n}\n\nexport function createLogger(module: string): PhotonLogger {\n return {\n debug: (message, attrs, error) =>\n emit(SeverityNumber.DEBUG, \"DEBUG\", module, message, attrs, error),\n info: (message, attrs, error) =>\n emit(SeverityNumber.INFO, \"INFO\", module, message, attrs, error),\n warn: (message, attrs, error) =>\n emit(SeverityNumber.WARN, \"WARN\", module, message, attrs, error),\n error: (message, attrs, error) =>\n emit(SeverityNumber.ERROR, \"ERROR\", module, message, attrs, error),\n };\n}\n","export const PHOTON_OTEL_VERSION = \"0.1.0\";\n","// E.164-ish phone match: optional `+`, 7–15 digits with optional separators.\nconst PHONE_PATTERN = /\\+?\\d[\\d\\s()\\-.]{6,18}\\d/g;\nconst EMAIL_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/g;\n\n/**\n * Mask a phone number, keeping the leading `+` (if any) plus the first 3 digits\n * and the last 4 digits visible. Example: `+13315553374` -> `+133xxxxx3374`.\n *\n * Inputs that don't have enough digits to safely mask are returned as\n * `xxxx` to avoid leaking the entire short value.\n */\nexport function sanitizePhone(input: string): string {\n const hasPlus = input.startsWith(\"+\");\n const digits = input.replace(/\\D/g, \"\");\n if (digits.length < 8) {\n return hasPlus ? \"+xxxx\" : \"xxxx\";\n }\n const head = digits.slice(0, 3);\n const tail = digits.slice(-4);\n const middleLength = digits.length - head.length - tail.length;\n return `${hasPlus ? \"+\" : \"\"}${head}${\"x\".repeat(middleLength)}${tail}`;\n}\n\n/**\n * Mask an email address, keeping the first 2 chars of the local part, the\n * first char of the domain, and the TLD. Example:\n * `foo.bar@example.com` -> `fo***@e***.com`.\n */\nexport function sanitizeEmail(input: string): string {\n const atIndex = input.lastIndexOf(\"@\");\n if (atIndex < 1) {\n return \"***\";\n }\n const local = input.slice(0, atIndex);\n const domain = input.slice(atIndex + 1);\n const dotIndex = domain.lastIndexOf(\".\");\n if (dotIndex < 1) {\n return \"***\";\n }\n const localHead = local.slice(0, 2);\n const domainHead = domain.slice(0, 1);\n const tld = domain.slice(dotIndex);\n return `${localHead}***@${domainHead}***${tld}`;\n}\n\n/**\n * Replace every phone number and email address inside a free-form string with\n * its sanitized form. Used to scrub `Error.message` values before attaching\n * them to span status.\n */\nexport function sanitizeErrorMessage(input: string): string {\n return input\n .replace(EMAIL_PATTERN, (match) => sanitizeEmail(match))\n .replace(PHONE_PATTERN, (match) => sanitizePhone(match));\n}\n","import { context, trace } from \"@opentelemetry/api\";\nimport { logs } from \"@opentelemetry/api-logs\";\nimport { AsyncLocalStorageContextManager } from \"@opentelemetry/context-async-hooks\";\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-http\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport {\n BatchLogRecordProcessor,\n LoggerProvider,\n} from \"@opentelemetry/sdk-logs\";\nimport {\n BasicTracerProvider,\n BatchSpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\nimport { type LogLevel, setLogLevel } from \"./logger\";\n\nexport interface SetupOtelOptions {\n /**\n * Default OTLP/HTTP base endpoint (e.g. `https://otel.example.com`). The\n * `/v1/traces` and `/v1/logs` paths are appended automatically. Standard\n * `OTEL_EXPORTER_OTLP_*` env vars always take precedence.\n */\n endpoint?: string;\n /**\n * Default OTLP headers (e.g. `{ Authorization: \"Basic ...\" }`). Merged with\n * any headers parsed from `OTEL_EXPORTER_OTLP_HEADERS`; env values win on\n * conflicts.\n */\n headers?: Record<string, string>;\n /**\n * Minimum log level emitted by `createLogger()` (to both OTLP and console).\n * The `LOG_LEVEL` env var still takes precedence. Defaults to `debug` in\n * development and `info` otherwise.\n */\n logLevel?: LogLevel;\n /**\n * Extra resource attributes attached to every span/log alongside\n * `service.name` / `service.version`.\n */\n resourceAttributes?: Record<string, string | number | boolean>;\n serviceName: string;\n serviceVersion?: string;\n}\n\nexport interface OtelHandle {\n shutdown(): Promise<void>;\n}\n\nlet activeHandle: OtelHandle | undefined;\n\nconst TRAILING_SLASH = /\\/$/;\n\nfunction parseEnvHeaders(raw: string | undefined): Record<string, string> {\n if (!raw) {\n return {};\n }\n const out: Record<string, string> = {};\n for (const pair of raw.split(\",\")) {\n const eq = pair.indexOf(\"=\");\n if (eq <= 0) {\n continue;\n }\n const key = pair.slice(0, eq).trim();\n const value = pair.slice(eq + 1).trim();\n if (key) {\n out[key] = value;\n }\n }\n return out;\n}\n\nfunction resolveTracesEndpoint(base: string | undefined): string | undefined {\n const traces = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT;\n if (traces) {\n return traces;\n }\n const generic = process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? base;\n return generic\n ? `${generic.replace(TRAILING_SLASH, \"\")}/v1/traces`\n : undefined;\n}\n\nfunction resolveLogsEndpoint(base: string | undefined): string | undefined {\n const logsEndpoint = process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT;\n if (logsEndpoint) {\n return logsEndpoint;\n }\n const generic = process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? base;\n return generic ? `${generic.replace(TRAILING_SLASH, \"\")}/v1/logs` : undefined;\n}\n\n/**\n * Boot an OTLP/HTTP-based OpenTelemetry pipeline (traces + logs).\n *\n * Idempotent: calling twice in the same process is a no-op on the second\n * call, so libraries can safely invoke this without clobbering an app-level\n * OTel setup that ran earlier.\n *\n * Standard `OTEL_EXPORTER_OTLP_*` env vars override the `endpoint` and\n * `headers` arguments — this matches the OpenTelemetry SDK config spec.\n */\nexport function setupOtel(options: SetupOtelOptions): OtelHandle {\n if (activeHandle) {\n return activeHandle;\n }\n\n if (options.logLevel) {\n setLogLevel(options.logLevel);\n }\n\n const tracesEndpoint = resolveTracesEndpoint(options.endpoint);\n const logsEndpoint = resolveLogsEndpoint(options.endpoint);\n const mergedHeaders = {\n ...options.headers,\n ...parseEnvHeaders(process.env.OTEL_EXPORTER_OTLP_HEADERS),\n };\n const hasHeaders = Object.keys(mergedHeaders).length > 0;\n\n const resource = resourceFromAttributes({\n \"service.name\": options.serviceName,\n ...(options.serviceVersion\n ? { \"service.version\": options.serviceVersion }\n : {}),\n \"deployment.environment\": process.env.DEPLOYMENT_ENV ?? \"development\",\n ...options.resourceAttributes,\n });\n\n context.setGlobalContextManager(new AsyncLocalStorageContextManager());\n\n const traceProcessors = tracesEndpoint\n ? [\n new BatchSpanProcessor(\n new OTLPTraceExporter({\n url: tracesEndpoint,\n headers: hasHeaders ? mergedHeaders : undefined,\n })\n ),\n ]\n : [];\n\n const tracerProvider = new BasicTracerProvider({\n resource,\n spanProcessors: traceProcessors,\n });\n trace.setGlobalTracerProvider(tracerProvider);\n\n const logProcessors = logsEndpoint\n ? [\n new BatchLogRecordProcessor(\n new OTLPLogExporter({\n url: logsEndpoint,\n headers: hasHeaders ? mergedHeaders : undefined,\n })\n ),\n ]\n : [];\n\n const loggerProvider = new LoggerProvider({\n resource,\n processors: logProcessors,\n });\n logs.setGlobalLoggerProvider(loggerProvider);\n\n const handle: OtelHandle = {\n async shutdown() {\n await Promise.allSettled([\n tracerProvider.shutdown(),\n loggerProvider.shutdown(),\n ]);\n activeHandle = undefined;\n },\n };\n\n activeHandle = handle;\n return handle;\n}\n\n/**\n * Read-only accessor for tests / debug paths that need to know whether\n * `setupOtel` has already run in this process.\n */\nexport function isOtelActive(): boolean {\n return activeHandle !== undefined;\n}\n","import {\n type Attributes,\n SpanStatusCode,\n type Tracer,\n trace,\n} from \"@opentelemetry/api\";\nimport type { LogAttrs } from \"./logger\";\nimport { sanitizeErrorMessage } from \"./sanitize\";\nimport { PHOTON_OTEL_VERSION } from \"./version\";\n\nlet scopedTracer: Tracer | undefined;\n\nfunction getTracer(): Tracer {\n if (!scopedTracer) {\n scopedTracer = trace.getTracer(\"@photon-ai/otel\", PHOTON_OTEL_VERSION);\n }\n return scopedTracer;\n}\n\nfunction toAttributes(attrs: LogAttrs): Attributes {\n const out: Attributes = {};\n for (const [k, v] of Object.entries(attrs)) {\n if (v !== undefined) {\n out[k] = v;\n }\n }\n return out;\n}\n\nexport function withSpan<T>(name: string, fn: () => Promise<T> | T): Promise<T>;\nexport function withSpan<T>(\n name: string,\n attrs: LogAttrs,\n fn: () => Promise<T> | T\n): Promise<T>;\nexport function withSpan<T>(\n name: string,\n attrsOrFn: LogAttrs | (() => Promise<T> | T),\n maybeFn?: () => Promise<T> | T\n): Promise<T> {\n const fn = typeof attrsOrFn === \"function\" ? attrsOrFn : maybeFn;\n if (!fn) {\n throw new Error(\"withSpan: function argument is required\");\n }\n const attrs = typeof attrsOrFn === \"function\" ? undefined : attrsOrFn;\n\n return getTracer().startActiveSpan(name, async (span) => {\n if (attrs) {\n span.setAttributes(toAttributes(attrs));\n }\n try {\n const result = await fn();\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (err) {\n span.recordException(err as Error);\n const errorObj = err instanceof Error ? err : undefined;\n span.setAttribute(\"error.type\", errorObj?.constructor.name ?? typeof err);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: errorObj\n ? sanitizeErrorMessage(errorObj.message)\n : sanitizeErrorMessage(String(err)),\n });\n throw err;\n } finally {\n span.end();\n }\n });\n}\n"],"mappings":"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","names":["logs","logs","trace","trace"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@photon-ai/otel",
3
- "version": "0.1.2",
3
+ "version": "1.0.0",
4
4
  "description": "DX-focused OpenTelemetry wrapper for Bun and Node.js: one-call setup, structured logging, span helper, and built-in PII scrubbing.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",