Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Vanilla OTel works, but the setup is verbose, the logger plumbing is awkward, an

- **`setupOtel()`** — idempotent one-call bootstrap for traces + logs + metrics. Honors standard `OTEL_EXPORTER_OTLP_*` env vars.
- **`otel.getMeter(name)`** — creates standard OpenTelemetry instruments from this setup's meter provider, with identical behavior in global and scoped mode.
- **`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`.
- **`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 shares one configurable level gate.
- **`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.
- **Automatic `fetch` tracing** — `setupOtel()` instruments outbound `fetch` so every request gets a CLIENT span and W3C trace-context headers. On **Node** it uses the official `@opentelemetry/instrumentation-undici`; on **Bun** — whose native fetch emits nothing for the standard `diagnostics_channel`-based instrumentations — it wraps `globalThis.fetch`. Pass `instrumentFetch: { mode: "global" }` to force the wrap on both for identical spans.
- **`sanitizeEmail` / `sanitizePhone` / `sanitizeErrorMessage`** — PII helpers you can reuse anywhere.
Expand Down Expand Up @@ -98,8 +98,8 @@ attribute guidance, and scoped mode.
| `instrumentFetch(options?): FetchInstrumentation` | Low-level wrap of `globalThis.fetch` for CLIENT spans + W3C propagation. Returns `{ unpatch() }`. `setupOtel` calls this on Bun; on Node it prefers native undici. |
| `createInstrumentedFetch(baseFetch?, options?): typeof fetch` | Returns a NEW instrumented fetch (CLIENT spans + W3C propagation) wrapping `baseFetch` (default `globalThis.fetch`) without touching the global. For SDKs that take a `fetch` option. |
| `createLogger(module): PhotonLogger` | Returns `{ info, warn, error, debug }`. Each call emits to OTel + `console`, correlates to active span. |
| `setLogLevel(level): void` | Set the minimum level emitted (`debug`/`info`/`warn`/`error`/`silent`). `LOG_LEVEL` env still wins. |
| `getLogLevel(): LogLevel` | Current effective level after env / override / default resolution. |
| `setLogLevel(level): void` | Set the minimum level emitted (`debug`/`info`/`warn`/`error`/`silent`). Programmatic configuration wins over `LOG_LEVEL`. |
| `getLogLevel(): LogLevel` | Current effective level after programmatic / env / default resolution. |
| `withSpan(name, fn)` | Wraps `fn` (sync or async) in a span. Records exceptions and scrubs PII in error messages. |
| `withSpan(name, attrs, fn)` | Same as above but attaches `attrs` to the span. |
| `sanitizeEmail(input)` | Masks an email: `foo.bar@example.com` → `fo***@e***.com`. |
Expand Down Expand Up @@ -134,22 +134,29 @@ sinks share one level gate.
Logs below the active level are dropped from **both** OTLP and the console. The level is
resolved fresh on every call, so changes take effect immediately:

1. `LOG_LEVEL` env var (`debug` | `info` | `warn` | `error` | `silent`) — wins if set.
2. `setLogLevel(level)` or `setupOtel({ logLevel })`.
3. Default: `debug` in development (`DEPLOYMENT_ENV` unset or `development`), `info` otherwise.
1. `setLogLevel(level)` or `setupOtel({ logLevel })`.
2. `LOG_LEVEL` env var (`debug` | `info` | `warn` | `error` | `silent`).
3. Default: `info`.

Logger configuration is independent of `DEPLOYMENT_ENV`; that variable only supplies
OpenTelemetry resource metadata. Environment values are trimmed and case-insensitive. An
invalid `LOG_LEVEL` falls back to `info` and warns once per distinct value, while an invalid
programmatic value throws a `TypeError` immediately (useful for untyped JavaScript callers).

```ts
import { setLogLevel } from "@photon-ai/otel";

setLogLevel("warn"); // debug + info now suppressed everywhere
// or set LOG_LEVEL=warn in the environment, which overrides the call above
// LOG_LEVEL is used only when no programmatic level has been set
```

`"silent"` suppresses everything, including errors.

## Configuration

Standard OpenTelemetry env vars always take precedence over `SetupOtelOptions`:
Standard OpenTelemetry exporter env vars take precedence over endpoint and
header options. Logger and deployment metadata variables follow the behavior
listed below:

| Variable | Effect |
| ----------------------------------------- | ------------------------------------------------------- |
Expand All @@ -161,8 +168,8 @@ Standard OpenTelemetry env vars always take precedence over `SetupOtelOptions`:
| `OTEL_EXPORTER_OTLP_<SIGNAL>_HEADERS` | Trace-, log-, or metric-specific headers; override generic and code headers. |
| `OTEL_METRIC_EXPORT_INTERVAL` | Metric export interval in milliseconds. Defaults to `60000`. |
| `OTEL_METRIC_EXPORT_TIMEOUT` | Metric export timeout in milliseconds. Defaults to `30000`. |
| `DEPLOYMENT_ENV` | Attached as `deployment.environment` resource attribute. Defaults to `development`. Also drives the default log level. |
| `LOG_LEVEL` | Minimum log level: `debug` \| `info` \| `warn` \| `error` \| `silent`. Overrides `setLogLevel()` / `setupOtel({ logLevel })`. |
| `DEPLOYMENT_ENV` | Attached as `deployment.environment` resource attribute. Defaults to `development`; does not affect logging. |
| `LOG_LEVEL` | Minimum log level: `debug` \| `info` \| `warn` \| `error` \| `silent`. Used when no programmatic level is set; defaults to `info`. |

## Automatic fetch instrumentation

Expand Down
17 changes: 12 additions & 5 deletions docs/concepts/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -90,19 +90,25 @@ Local development should not require infrastructure.

When no endpoint is configured, the package still installs providers but uses empty processor and reader lists. Application code can call `withSpan()`, `createLogger()`, and `otel.getMeter()` without branching on environment.

This design means the same code path runs locally and in production. The deployment environment decides whether telemetry is exported.
This design means the same code path runs locally and in production. Deployment
configuration decides whether telemetry is exported.

## Why environment variables win
## Why exporter environment variables win

Standard OpenTelemetry environment variables override code-level options.
Standard OpenTelemetry exporter environment variables override code-level
endpoint and header options.

This follows OpenTelemetry configuration expectations and keeps deployment concerns outside application code.

For example:

- production can inject real exporter credentials through `OTEL_EXPORTER_OTLP_HEADERS`
- staging can point to a different collector through `OTEL_EXPORTER_OTLP_ENDPOINT`
- incident response can raise or lower `LOG_LEVEL` without a code deploy
- incident response can raise or lower `LOG_LEVEL` without a code deploy when the host application has not set a programmatic level

Logger configuration intentionally follows a different SDK-first rule:
`setLogLevel()` or `setupOtel({ logLevel })` wins over `LOG_LEVEL`, and the
unconfigured default is always `info`.

## Logging architecture

Expand All @@ -112,7 +118,8 @@ This deliberately avoids choosing between local developer ergonomics and structu

The log-level gate runs before both sinks. A suppressed debug message does not reach OpenTelemetry and does not reach the console.

The logger resolves the active level on each call so environment or programmatic changes take effect immediately.
The logger resolves the active level on each call so environment changes take
effect immediately until a programmatic override is active.

## Span helper architecture

Expand Down
33 changes: 21 additions & 12 deletions docs/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ title: "Configuration"
description: "Configure service identity, OTLP endpoints, headers, resource attributes, fetch instrumentation, and log levels."
---

`setupOtel()` accepts code-level defaults, while standard OpenTelemetry environment variables keep final control in deployment environments.
`setupOtel()` accepts application-owned configuration. Standard OpenTelemetry
environment variables keep final control over exporter destinations and
credentials, while an explicit SDK log level remains authoritative.

That split is intentional:

- Application code owns stable identity like `serviceName`.
- Deployment configuration owns destinations, credentials, and runtime log level.
- Application code owns stable identity and explicit SDK behavior such as `serviceName` and `logLevel`.
- Deployment configuration owns destinations and credentials, and can supply `LOG_LEVEL` when code leaves the level unset.

## Basic options

Expand Down Expand Up @@ -142,14 +144,12 @@ Avoid putting per-request values in resource attributes. Use span attributes or

If unset, it defaults to `development`.

It also affects the default log level:

- `debug` when `DEPLOYMENT_ENV` is unset or `development`
- `info` in every other environment
This value only describes telemetry resource metadata. It does not affect the
logger level.

## Log level

You can set a code default:
You can set the application-owned level in code:

```ts
setupOtel({
Expand All @@ -158,13 +158,17 @@ setupOtel({
});
```

Or set `LOG_LEVEL`:
Or use `LOG_LEVEL` when the host application does not set a level:

```bash
LOG_LEVEL=warn
```

`LOG_LEVEL` wins over `setupOtel({ logLevel })` and `setLogLevel()`.
Resolution order is:

1. `setLogLevel()` or `setupOtel({ logLevel })`
2. a valid `LOG_LEVEL`
3. `info`

Allowed values are:

Expand All @@ -176,6 +180,11 @@ Allowed values are:

`silent` suppresses all logs, including errors.

Environment values are trimmed and case-insensitive; an empty value is treated
as unset. An invalid `LOG_LEVEL` warns once per distinct normalized value and
falls back to `info`. Invalid programmatic values from untyped JavaScript callers
throw a `TypeError` before changing logger or setup state.

## Fetch instrumentation

By default, `setupOtel()` instruments `fetch` when a traces endpoint is configured.
Expand Down Expand Up @@ -248,7 +257,7 @@ The package always excludes its own OTLP exporter endpoints from fetch instrumen
| Log endpoint | `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | base endpoint + `/v1/logs` |
| Metric endpoint | `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | base endpoint + `/v1/metrics` |
| Signal headers | signal-specific `*_HEADERS` | generic headers, then `setupOtel({ headers })` |
| Log level | `LOG_LEVEL` | `setupOtel({ logLevel })` or `setLogLevel()` |
| Log level | `setupOtel({ logLevel })` or `setLogLevel()` | `LOG_LEVEL`, then `info` |
| Deployment environment | `DEPLOYMENT_ENV` | `development` |

## Best practices
Expand All @@ -257,5 +266,5 @@ The package always excludes its own OTLP exporter endpoints from fetch instrumen
- Always set `serviceName`.
- Set `serviceVersion` from your release artifact when possible.
- Prefer stable resource attributes over high-cardinality request values.
- Use `LOG_LEVEL=debug` temporarily when debugging production incidents, then return to `info` or higher.
- Use `LOG_LEVEL=debug` temporarily only when the application has not set a programmatic level; otherwise change the explicit SDK configuration.
- Avoid putting secrets in URL query strings (fetch spans include `url.full`); when unavoidable, strip them with the `redactUrl` option and the `sanitizeUrl()` helper.
20 changes: 12 additions & 8 deletions docs/guides/logging.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -97,21 +97,25 @@ The active level is resolved fresh on every log call.

Resolution order:

1. `LOG_LEVEL`
2. `setLogLevel()` or `setupOtel({ logLevel })`
3. default level from `DEPLOYMENT_ENV`
1. `setLogLevel()` or `setupOtel({ logLevel })`
2. a valid `LOG_LEVEL`
3. `info`

```ts
import { getLogLevel, setLogLevel } from "@photon-ai/otel";

setLogLevel("warn");
console.log(getLogLevel()); // "warn", unless LOG_LEVEL is set
console.log(getLogLevel()); // "warn", even if LOG_LEVEL is set
```

Defaults:
`DEPLOYMENT_ENV` only supplies the `deployment.environment` telemetry resource
attribute. It never changes the logger level.

- `debug` when `DEPLOYMENT_ENV` is unset or `development`
- `info` otherwise
Environment values are trimmed and case-insensitive. Empty values behave as
unset. An invalid active `LOG_LEVEL` warns once per distinct normalized value
and falls back to `info`; it is not inspected when a programmatic level is
active. Invalid programmatic values from untyped JavaScript callers throw a
`TypeError` immediately.

Allowed values:

Expand Down Expand Up @@ -187,5 +191,5 @@ In a backend that supports trace/log correlation, that log can be opened from th
- Use stable module names.
- Attach attributes for business identifiers and operational context.
- Pass exceptions as the third argument instead of stringifying them.
- Set `LOG_LEVEL` in deployment configuration.
- Prefer a programmatic level when the host application owns logging policy; use `LOG_LEVEL` as the deployment fallback otherwise.
- Avoid logging raw PII, secrets, or full request bodies.
8 changes: 6 additions & 2 deletions docs/reference/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ interface SetupOtelOptions {
- Installs W3C trace-context and baggage propagation.
- Configures OTLP/HTTP trace, log, and metric exporters when endpoints are available.
- Supports no-endpoint local development.
- Applies `logLevel` as a programmatic override above `LOG_LEVEL`; defaults to `info` when neither is configured.
- Optionally instruments fetch.
- Returns an `OtelHandle`.

Expand Down Expand Up @@ -141,7 +142,8 @@ Sets the programmatic minimum log level.
function setLogLevel(level: LogLevel): void;
```

`LOG_LEVEL` still wins when it is set.
This setting takes precedence over `LOG_LEVEL`. Untyped JavaScript callers that
pass an invalid value receive a `TypeError` before the active level changes.

## `getLogLevel()`

Expand All @@ -151,7 +153,9 @@ Returns the current effective log level.
function getLogLevel(): LogLevel;
```

The value is resolved from `LOG_LEVEL`, then programmatic override, then deployment default.
The value is resolved from programmatic configuration, then a valid
`LOG_LEVEL`, then the unconditional `info` default. `DEPLOYMENT_ENV` does not
participate in logger resolution.

## `LogLevel`

Expand Down
25 changes: 14 additions & 11 deletions docs/reference/environment.mdx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
---
title: "Environment reference"
description: "Environment variables recognized by @photon-ai/otel and how they override code options."
description: "Environment variables recognized by @photon-ai/otel and their precedence and fallback behavior."
---

This package follows standard OpenTelemetry environment variable precedence for OTLP exporter configuration.

Environment variables are deployment-owned configuration. They override code defaults where applicable.
Environment variables are deployment-owned configuration. Standard
OpenTelemetry exporter variables override code defaults where applicable;
`LOG_LEVEL` is used only when no programmatic logger level is active.

## Summary

| Variable | Purpose | Overrides |
| Variable | Purpose | Precedence or fallback |
| --- | --- | --- |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Base OTLP/HTTP endpoint | `setupOtel({ endpoint })` |
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | Full traces endpoint | base endpoint for traces |
Expand All @@ -19,8 +21,8 @@ Environment variables are deployment-owned configuration. They override code def
| `OTEL_EXPORTER_OTLP_<SIGNAL>_HEADERS` | Signal-specific exporter headers | generic and code headers |
| `OTEL_METRIC_EXPORT_INTERVAL` | Metric export interval in milliseconds | default `60000` |
| `OTEL_METRIC_EXPORT_TIMEOUT` | Metric export timeout in milliseconds | default `30000` |
| `DEPLOYMENT_ENV` | deployment resource attribute and default log-level input | default `development` |
| `LOG_LEVEL` | logger minimum severity | `setupOtel({ logLevel })` and `setLogLevel()` |
| `DEPLOYMENT_ENV` | deployment resource attribute | default `development` |
| `LOG_LEVEL` | logger minimum severity | programmatic level wins; otherwise default `info` |

## `OTEL_EXPORTER_OTLP_ENDPOINT`

Expand Down Expand Up @@ -118,10 +120,7 @@ The value is attached to telemetry as `deployment.environment`.

If unset, the package uses `development`.

It also controls the default log level:

- `development` or unset: `debug`
- any other value: `info`
It does not participate in logger-level resolution.

## `LOG_LEVEL`

Expand All @@ -139,9 +138,13 @@ Allowed values:
- `error`
- `silent`

Invalid values are ignored.
Values are trimmed and case-insensitive. Empty values are treated as unset.
Invalid values warn once per distinct normalized value and fall back to `info`.

`LOG_LEVEL` is checked on every log call and wins over both `setupOtel({ logLevel })` and `setLogLevel()`.
When no programmatic level is active, `LOG_LEVEL` is checked on every log call
so deployment changes take effect immediately. Both `setupOtel({ logLevel })`
and `setLogLevel()` take precedence over it. If neither source supplies a valid
level, the logger defaults to `info`.
Comment on lines +144 to +147

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clarify that only in-process environment mutations are observed live.

A deployment-side environment update does not mutate an already-running Node/Bun process. State that each call rereads process.env.LOG_LEVEL; deployment changes require restart/redeploy unless application code updates process.env.

  • docs/reference/environment.mdx#L144-L147: replace “deployment changes take effect immediately” with the process-local behavior.
  • docs/concepts/architecture.mdx#L121-L122: qualify “environment changes” as changes to the running process’s process.env.
📍 Affects 2 files
  • docs/reference/environment.mdx#L144-L147 (this comment)
  • docs/concepts/architecture.mdx#L121-L122
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/reference/environment.mdx` around lines 144 - 147, Clarify the logging
environment behavior in docs/reference/environment.mdx lines 144-147: each log
call rereads the running process’s process.env.LOG_LEVEL, so only in-process
mutations are observed live; deployment-side changes require a restart or
redeploy. Also qualify the “environment changes” wording in
docs/concepts/architecture.mdx lines 121-122 to explicitly mean changes to the
running process’s process.env.


## Local development example

Expand Down
Loading
Loading