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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ jobs:
- name: Pack and install gate
run: bash scripts/pack-and-install.sh

- name: OTel pack-and-import smoke test
run: bash scripts/otel-smoke.sh

- name: Generated profiles drift check
run: pnpm --filter @peac/policy-kit generate:profiles:check

Expand Down
6 changes: 3 additions & 3 deletions examples/telemetry-otel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
},
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/sdk-metrics": "^1.29.0",
"@opentelemetry/sdk-trace-base": "^1.29.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.57.1",
"@opentelemetry/sdk-metrics": "^2.0.0",
"@opentelemetry/sdk-trace-base": "^2.0.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.200.0",
"@peac/protocol": "workspace:*",
"@peac/telemetry": "workspace:*",
"@peac/telemetry-otel": "workspace:*",
Expand Down
5 changes: 3 additions & 2 deletions examples/telemetry-otel/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ import { createOtelProvider } from '@peac/telemetry-otel';
*/
function initOtel(): void {
// Set up trace provider with console exporter (for demo)
const tracerProvider = new BasicTracerProvider();
tracerProvider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
const tracerProvider = new BasicTracerProvider({
spanProcessors: [new SimpleSpanProcessor(new ConsoleSpanExporter())],
});
trace.setGlobalTracerProvider(tracerProvider);

console.log('[OTel] Tracing initialized with console exporter\n');
Expand Down
3 changes: 2 additions & 1 deletion packages/privacy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"repository": {
Expand Down
22 changes: 21 additions & 1 deletion packages/telemetry-otel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ This package bridges PEAC telemetry events to OpenTelemetry spans, events, and m
pnpm add @peac/telemetry-otel @opentelemetry/api
```

Note: `@opentelemetry/api` is a peer dependency. You must install it along with your OTel SDK components.
Note: `@opentelemetry/api` is the only runtime peer dependency. OTel SDK packages
(`@opentelemetry/sdk-trace-base`, `@opentelemetry/sdk-metrics`) are **not required** --
this package uses only the API facade and works with any compatible SDK the consumer provides.

## Usage

Expand Down Expand Up @@ -123,6 +125,24 @@ const extensions = createTraceContextExtensions(req.headers);
- `recordReceiptVerified(metrics, ...)` - Record receipt verified
- `recordAccessDecision(metrics, ...)` - Record access decision

## Compatibility Matrix

| Dependency | Role | Version | Notes |
| ------------------------------- | --------------------- | ---------- | ------------------------------------------ |
| `@opentelemetry/api` | Peer dep (production) | `^1.9.0` | Stable API, backward-compatible across 1.x |
| `@opentelemetry/sdk-metrics` | Dev dep (tests only) | `^2.0.0` | SDK v2 -- NOT shipped to consumers |
| `@opentelemetry/sdk-trace-base` | Dev dep (tests only) | `^2.0.0` | SDK v2 -- NOT shipped to consumers |
| Node.js | Runtime | `>=22.0.0` | Matches monorepo `engines.node` |

**Key points:**

- `@peac/telemetry-otel` only depends on `@opentelemetry/api` at runtime (peer dep).
Consumers bring their own SDK and exporter versions.
- SDK v2 packages are dev dependencies used for testing. They are NOT bundled
into the published package and do not appear in consumers' dependency trees.
- The OTel exporter in `examples/telemetry-otel/` uses `@opentelemetry/exporter-trace-otlp-http@^0.200.0`.
This version is coupled to SDK v2 -- OTel uses a `0.{MAJOR}xx.x` scheme for experimental packages.

## Related Packages

- `@peac/telemetry` - Core interfaces and no-op provider
Expand Down
16 changes: 11 additions & 5 deletions packages/telemetry-otel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"files": [
Expand All @@ -33,12 +34,17 @@
"@peac/telemetry": "workspace:*"
},
"peerDependencies": {
"@opentelemetry/api": "^1.7.0"
"@opentelemetry/api": "^1.9.0"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": false
}
},
"devDependencies": {
"@opentelemetry/api": "^1.7.0",
"@opentelemetry/sdk-metrics": "^1.29.0",
"@opentelemetry/sdk-trace-base": "^1.29.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/sdk-metrics": "^2.0.0",
"@opentelemetry/sdk-trace-base": "^2.0.0",
"vitest": "^4.0.0"
},
"publishConfig": {
Expand Down
119 changes: 119 additions & 0 deletions packages/telemetry-otel/tests/import-smoke.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* Import smoke test -- verifies @peac/telemetry-otel works with only
* @opentelemetry/api installed (no SDK packages).
*
* The OTel API provides no-op implementations by default. This test
* proves the package does NOT require @opentelemetry/sdk-metrics or
* @opentelemetry/sdk-trace-base at runtime.
*/

import { describe, it, expect } from 'vitest';

describe('import without SDK', () => {
it('all public exports resolve', async () => {
const mod = await import('../src/index.js');

// Main provider
expect(typeof mod.createOtelProvider).toBe('function');

// Trace context
expect(typeof mod.validateTraceparent).toBe('function');
expect(typeof mod.parseTraceparent).toBe('function');
expect(typeof mod.isSampled).toBe('function');
expect(typeof mod.extractTraceparentFromHeaders).toBe('function');
expect(typeof mod.extractTracestateFromHeaders).toBe('function');
expect(typeof mod.createTraceContextExtensions).toBe('function');
expect(mod.TRACE_CONTEXT_KEYS).toBeDefined();

// Privacy
expect(typeof mod.createPrivacyFilter).toBe('function');
expect(typeof mod.hashIssuer).toBe('function');
expect(typeof mod.hashKid).toBe('function');
expect(typeof mod.shouldEmitAttribute).toBe('function');

// Metrics
expect(typeof mod.createMetrics).toBe('function');
expect(typeof mod.recordReceiptIssued).toBe('function');
expect(typeof mod.recordReceiptVerified).toBe('function');
expect(typeof mod.recordAccessDecision).toBe('function');
expect(mod.METRIC_NAMES).toBeDefined();

// Version constant
expect(typeof mod.TELEMETRY_OTEL_VERSION).toBe('string');
});

it('createOtelProvider works with no-op API (no SDK registered)', async () => {
// No SDK setup -- the API provides no-op tracer/meter by default
const { createOtelProvider } = await import('../src/index.js');

const provider = createOtelProvider({
serviceName: 'smoke-test',
privacyMode: 'strict',
});

expect(provider).toBeDefined();
expect(typeof provider.onReceiptIssued).toBe('function');
expect(typeof provider.onReceiptVerified).toBe('function');
expect(typeof provider.onAccessDecision).toBe('function');
});

it('provider methods do not throw with no-op API', async () => {
const { createOtelProvider } = await import('../src/index.js');

const provider = createOtelProvider({
serviceName: 'smoke-test',
privacyMode: 'strict',
});

// All methods should succeed silently with no-op meter/tracer
expect(() => {
provider.onReceiptIssued({
receiptHash: 'abc123',
issuer: 'https://example.com',
kid: 'key-001',
durationMs: 42,
});
}).not.toThrow();

expect(() => {
provider.onReceiptVerified({
receiptHash: 'abc123',
valid: true,
issuer: 'https://example.com',
durationMs: 10,
});
}).not.toThrow();

expect(() => {
provider.onAccessDecision({
decision: 'allow',
receiptHash: 'abc123',
reasonCode: 'valid_receipt',
});
}).not.toThrow();
});

it('package.json: SDK packages are devDependencies only', async () => {
const { readFileSync } = await import('node:fs');
const { resolve } = await import('node:path');

const pkgPath = resolve(__dirname, '../package.json');
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));

// @opentelemetry/api must be a peer dependency
expect(pkg.peerDependencies['@opentelemetry/api']).toBeDefined();

// SDK packages must NOT be in dependencies or peerDependencies
const deps = pkg.dependencies ?? {};
const peerDeps = pkg.peerDependencies ?? {};

expect(deps['@opentelemetry/sdk-metrics']).toBeUndefined();
expect(deps['@opentelemetry/sdk-trace-base']).toBeUndefined();
expect(peerDeps['@opentelemetry/sdk-metrics']).toBeUndefined();
expect(peerDeps['@opentelemetry/sdk-trace-base']).toBeUndefined();

// SDK packages should be in devDependencies
expect(pkg.devDependencies['@opentelemetry/sdk-metrics']).toBeDefined();
expect(pkg.devDependencies['@opentelemetry/sdk-trace-base']).toBeDefined();
});
});
5 changes: 3 additions & 2 deletions packages/telemetry-otel/tests/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ describe('createOtelProvider', () => {
beforeEach(() => {
// Set up trace provider with in-memory exporter
spanExporter = new InMemorySpanExporter();
tracerProvider = new BasicTracerProvider();
tracerProvider.addSpanProcessor(new SimpleSpanProcessor(spanExporter));
tracerProvider = new BasicTracerProvider({
spanProcessors: [new SimpleSpanProcessor(spanExporter)],
});
trace.setGlobalTracerProvider(tracerProvider);

// Set up meter provider
Expand Down
Loading
Loading