@friggframework/core 2.0.0--canary.622.eb2c9bd.0 → 2.0.0--canary.622.ce0381c.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 (43) hide show
  1. package/README.md +1 -1
  2. package/application/commands/usage-commands.js +13 -33
  3. package/application/index.js +5 -9
  4. package/core/create-handler.js +2 -2
  5. package/handlers/integration-event-dispatcher.js +1 -1
  6. package/index.js +1 -1
  7. package/integrations/integration-base.js +5 -5
  8. package/modules/requester/requester.js +2 -2
  9. package/package.json +5 -5
  10. package/reporting/reporting-router.js +1 -1
  11. package/reporting/use-cases/list-integrations-report.js +3 -3
  12. package/telemetry/README.md +25 -11
  13. package/telemetry/bind-telemetry-context.js +1 -1
  14. package/telemetry/canonical-counters.js +1 -1
  15. package/telemetry/exporters/console-exporter.js +19 -0
  16. package/telemetry/exporters/datadog-exporter.js +11 -0
  17. package/telemetry/exporters/honeycomb-exporter.js +13 -0
  18. package/telemetry/exporters/otlp-exporter.js +42 -0
  19. package/telemetry/exporters/passthrough-exporter.js +25 -0
  20. package/telemetry/exporters/resolve-exporter.js +39 -0
  21. package/telemetry/exporters/telemetry-exporter-interface.js +17 -0
  22. package/telemetry/index.js +8 -4
  23. package/telemetry/instrument-handler.js +1 -1
  24. package/telemetry/no-op-telemetry.js +61 -56
  25. package/telemetry/north-star.js +2 -2
  26. package/telemetry/otel-telemetry.js +160 -157
  27. package/telemetry/plugin-subscribers-singleton.js +2 -2
  28. package/telemetry/plugin-subscribers.js +1 -1
  29. package/telemetry/telemetry-config.js +4 -2
  30. package/telemetry/telemetry-context.js +1 -1
  31. package/telemetry/telemetry-event-bus.js +2 -2
  32. package/telemetry/telemetry-service-interface.js +49 -0
  33. package/telemetry/telemetry-service.js +10 -12
  34. package/telemetry/telemetry-singleton.js +3 -3
  35. package/telemetry/usage-rollup-singleton.js +1 -1
  36. package/telemetry/usage-rollup-subscriber.js +3 -3
  37. package/telemetry/usage-windows.js +1 -1
  38. package/usage/README.md +5 -5
  39. package/usage/repositories/usage-repository-documentdb.js +6 -6
  40. package/usage/repositories/usage-repository-interface.js +5 -5
  41. package/usage/repositories/usage-repository-postgres.js +6 -6
  42. package/usage/tracked-metrics.js +1 -1
  43. package/telemetry/exporters/exporter-factory.js +0 -79
package/README.md CHANGED
@@ -232,7 +232,7 @@ class HubSpotIntegration extends IntegrationBase {
232
232
 
233
233
  // Read the durable usage store (never an APM):
234
234
  const frigg = createFriggCommands({ integrationClass: HubSpotIntegration });
235
- await frigg.usage.totals({ metric: 'records.synced', groupBy: 'integrationType' });
235
+ await frigg.usage.getTotalsByDimension({ metric: 'records.synced', groupBy: 'integrationType' });
236
236
  ```
237
237
 
238
238
  **See:** [`telemetry/README.md`](telemetry/README.md) for the full guide
@@ -1,31 +1,15 @@
1
- /**
2
- * Usage Commands
3
- *
4
- * Application Layer — the read/write surface over the durable usage store
5
- * (ADR-011 §5). Exposed as `frigg.usage.*` on the unified command object so
6
- * reports and integration code query usage without touching the repository.
7
- *
8
- * @example
9
- * const frigg = createFriggCommands({ integrationClass: MyIntegration });
10
- * await frigg.usage.totals({ metric: 'records.synced', groupBy: 'integrationType', since });
11
- * await frigg.usage.series({ metric: 'records.synced', integrationType: 'hubspot', bucket: 'day' });
12
- */
1
+ // `frigg.usage.*` — the read/write surface over the durable usage store.
13
2
  const {
14
3
  createUsageRepository,
15
4
  } = require('../../usage/repositories/usage-repository-factory');
16
5
  const { computeUsageWindows } = require('../../telemetry/usage-windows');
17
6
  const { resolveNorthStarEntry } = require('../../telemetry/north-star');
18
7
 
19
- function createUsageCommands({ usageRepository, northStar = null } = {}) {
8
+ function createUsageCommands({ usageRepository } = {}) {
20
9
  const repository = usageRepository || createUsageRepository();
21
10
 
22
11
  return {
23
- /**
24
- * Record a usage counter for a point in time. Callers pass `at` (a Date,
25
- * default now) — NOT a raw window key — and both the day and hour windows
26
- * are derived, matching how the auto-rollup persists so series() reads
27
- * back consistently at either granularity.
28
- */
12
+ // Increments both the day and hour window rows for `at`.
29
13
  async recordUsageCounter({
30
14
  integrationId,
31
15
  integrationType,
@@ -33,7 +17,8 @@ function createUsageCommands({ usageRepository, northStar = null } = {}) {
33
17
  value = 1,
34
18
  at = new Date(),
35
19
  }) {
36
- for (const window of computeUsageWindows(at)) {
20
+ const windows = computeUsageWindows(at);
21
+ for (const window of windows) {
37
22
  await repository.increment({
38
23
  integrationId,
39
24
  integrationType,
@@ -44,25 +29,20 @@ function createUsageCommands({ usageRepository, northStar = null } = {}) {
44
29
  }
45
30
  },
46
31
 
47
- async totals(args) {
48
- return repository.totals(args);
32
+ async getTotalsByDimension(args) {
33
+ return repository.getTotalsByDimension(args);
49
34
  },
50
35
 
51
- async series(args) {
52
- return repository.series(args);
36
+ async getTimeSeries(args) {
37
+ return repository.getTimeSeries(args);
53
38
  },
54
39
 
55
- /**
56
- * First-class North Star read (ADR-011 Decision 5). Resolves the
57
- * configured counter for an integration type (byType wins over default),
58
- * then returns its totals from the durable usage store. Returns `null`
59
- * when no North Star is configured, so callers can branch without
60
- * knowing the counter key. Trends read via `series({ metric })`.
61
- */
62
- async northStar({ integrationType, since, groupBy = 'integrationType', bucket } = {}) {
40
+ // Caller supplies the North Star config (Definition.telemetry.northStar);
41
+ // resolves the counter for the type (byType > default), null if none.
42
+ async getNorthStarTotals({ northStar, integrationType, since, groupBy = 'integrationType', bucket } = {}) {
63
43
  const entry = resolveNorthStarEntry(northStar, integrationType);
64
44
  if (!entry) return null;
65
- const totals = await repository.totals({
45
+ const totals = await repository.getTotalsByDimension({
66
46
  metric: entry.name,
67
47
  groupBy,
68
48
  since,
@@ -17,11 +17,6 @@ const { createUsageCommands } = require('./commands/usage-commands');
17
17
  *
18
18
  * @param {Object} params
19
19
  * @param {Object} params.integrationClass - Integration class (required)
20
- * @param {Object} [params.northStar] - Resolved North Star config
21
- * (`Definition.telemetry.northStar`) so `frigg.usage.northStar(...)` can read
22
- * it (ADR-011 Decision 5). Injected by the caller that owns the app definition
23
- * (the handler/report runner) — the application layer never reaches up to load
24
- * it. Omitted → usage reads report no North Star.
25
20
  * @returns {Object} Unified commands object with all CRUD operations
26
21
  *
27
22
  * @example
@@ -29,7 +24,7 @@ const { createUsageCommands } = require('./commands/usage-commands');
29
24
  * const user = await commands.createUser({ username: 'user@example.com' });
30
25
  * const credential = await commands.createCredential({ userId: user.id, ... });
31
26
  */
32
- function createFriggCommands({ integrationClass, northStar = null }) {
27
+ function createFriggCommands({ integrationClass }) {
33
28
  // All commands use Frigg's default repositories and use cases
34
29
  const integrationCommands = createIntegrationCommands({ integrationClass });
35
30
 
@@ -57,9 +52,10 @@ function createFriggCommands({ integrationClass, northStar = null }) {
57
52
  // Process commands
58
53
  ...processCommands,
59
54
 
60
- // Usage read/write (ADR-011) — nested to match `frigg.usage.*`.
61
- // northStar injected by the caller (composition root), not loaded here.
62
- usage: createUsageCommands({ northStar }),
55
+ // Usage read/write — nested to match `frigg.usage.*`. The North Star
56
+ // read takes its config as a call argument, so nothing telemetry-specific
57
+ // is threaded through this general factory.
58
+ usage: createUsageCommands(),
63
59
  };
64
60
  }
65
61
 
@@ -153,8 +153,8 @@ const createHandler = (optionByName = {}) => {
153
153
  ? usageRollup
154
154
  : getUsageRollupSubscriber();
155
155
 
156
- // Wire adopter-declared telemetry subscribers once per cold start
157
- // (ADR-011 Decision 6). Memoized in the singleton, so this is a cheap
156
+ // Wire adopter-declared telemetry subscribers once per cold start.
157
+ // Memoized in the singleton, so this is a cheap
158
158
  // no-op after the first invocation.
159
159
  getPluginTelemetrySubscribers();
160
160
 
@@ -25,7 +25,7 @@ class IntegrationEventDispatcher {
25
25
  }
26
26
 
27
27
  /**
28
- * Resolve + invoke a handler, auto-instrumented (ADR-011 Decision 2). This
28
+ * Resolve + invoke a handler, auto-instrumented. This
29
29
  * is the seam for queue/webhook/defined-route dispatch; the `this.on` path
30
30
  * (user actions, lifecycle) is instrumented in IntegrationBase.send().
31
31
  */
package/index.js CHANGED
@@ -143,7 +143,7 @@ module.exports = {
143
143
  createReportingRouter,
144
144
  createReportingRepository,
145
145
 
146
- // telemetry (ADR-011)
146
+ // telemetry
147
147
  createTelemetry,
148
148
  getTelemetry,
149
149
  createUsageRepository,
@@ -79,7 +79,7 @@ class IntegrationBase {
79
79
  // Tier 3 Integration Extensions — see packages/core/integrations/EXTENSIONS.md
80
80
  // Shape: { [bindingName]: { extension, handlers?: { [eventName]: methodName } } }
81
81
  extensions: {},
82
- // Usage-counter opt-in (ADR-011). Declaring a canonical key opts into the
82
+ // Usage-counter opt-in. Declaring a canonical key opts into the
83
83
  // cross-integration comparison report + durable rollup; custom keys are
84
84
  // comparable within this integration type. Shape:
85
85
  // usage: { canonical: ['records.synced', ...], custom: { 'deals.enriched': { unit, label } } }
@@ -108,7 +108,7 @@ class IntegrationBase {
108
108
  this.messages = { errors: [], warnings: [] };
109
109
  this._isHydrated = false;
110
110
 
111
- // Telemetry (ADR-011): every instance carries the service so integration
111
+ // Telemetry: every instance carries the service so integration
112
112
  // code can call `this.telemetry.*`. Extracted before the record check so
113
113
  // passing only `telemetry` never triggers a hollow hydration. Bound to
114
114
  // this instance so emissions auto-carry `integration_type` for the usage
@@ -223,7 +223,7 @@ class IntegrationBase {
223
223
 
224
224
  this._isHydrated = Boolean(this.id);
225
225
 
226
- // ADR-011 Decision 2: log the instance-open exactly once per hydrated
226
+ // Log the instance-open exactly once per hydrated
227
227
  // instance, carrying the standard identifier set — so an integration is
228
228
  // visible in telemetry even on a path that never dispatches a handler.
229
229
  // High-cardinality ids ride the bus context (3rd arg), never metric
@@ -249,7 +249,7 @@ class IntegrationBase {
249
249
  }
250
250
 
251
251
  /**
252
- * Standard telemetry identifier set (ADR-011 Decision 3). Assembled from the
252
+ * Standard telemetry identifier set. Assembled from the
253
253
  * hydrated record (integrationId, userId, version), the static Definition
254
254
  * (integrationType, version fallback), and the environment (stage, appName).
255
255
  * High-cardinality ids (integrationId, userId) ride span baggage only — never
@@ -812,7 +812,7 @@ class IntegrationBase {
812
812
  `Event ${event} is not defined in the Integration event object`
813
813
  );
814
814
  }
815
- // Auto-instrument (ADR-011 Decision 2). This is the seam for user
815
+ // Auto-instrument. This is the seam for user
816
816
  // actions, config-options, and lifecycle events dispatched via `this.on`
817
817
  // (the queue/webhook/route paths go through IntegrationEventDispatcher).
818
818
  return instrumentHandler(
@@ -44,7 +44,7 @@ class Requester extends Delegate {
44
44
  // Instance methods can use this.fetch without differentiating
45
45
  this.fetch = get(params, 'fetch', fetch);
46
46
 
47
- // Telemetry (ADR-011). Defaults to the process singleton; overridable
47
+ // Telemetry. Defaults to the process singleton; overridable
48
48
  // for tests. Outbound requests are instrumented in `_request`.
49
49
  //
50
50
  // Attribution boundary: the `frigg.apimodule.requests` OTel metric fires
@@ -103,7 +103,7 @@ class Requester extends Delegate {
103
103
  };
104
104
 
105
105
  /**
106
- * Instrumenting entry point (ADR-011 P7). Wraps the whole logical request —
106
+ * Instrumenting entry point. Wraps the whole logical request —
107
107
  * including retry/refresh recursion — in a single span + one
108
108
  * `frigg.apimodule.requests` counter, emitted on the `i === 0` boundary so
109
109
  * retries are never double-counted. The full URL rides the span only; the
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.622.eb2c9bd.0",
4
+ "version": "2.0.0--canary.622.ce0381c.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -45,9 +45,9 @@
45
45
  }
46
46
  },
47
47
  "devDependencies": {
48
- "@friggframework/eslint-config": "2.0.0--canary.622.eb2c9bd.0",
49
- "@friggframework/prettier-config": "2.0.0--canary.622.eb2c9bd.0",
50
- "@friggframework/test": "2.0.0--canary.622.eb2c9bd.0",
48
+ "@friggframework/eslint-config": "2.0.0--canary.622.ce0381c.0",
49
+ "@friggframework/prettier-config": "2.0.0--canary.622.ce0381c.0",
50
+ "@friggframework/test": "2.0.0--canary.622.ce0381c.0",
51
51
  "@prisma/client": "^6.19.3",
52
52
  "@types/lodash": "4.17.15",
53
53
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -87,5 +87,5 @@
87
87
  "publishConfig": {
88
88
  "access": "public"
89
89
  },
90
- "gitHead": "eb2c9bde95b23f2f854fd5d78b2c5388ac5222d8"
90
+ "gitHead": "ce0381c83a8fdebf49352eb9cd6b38b360130025"
91
91
  }
@@ -40,7 +40,7 @@ function buildTypeLabels() {
40
40
 
41
41
  function createReportingRouter() {
42
42
  const reportingRepository = createReportingRepository();
43
- // ADR-011 hand-off: enrich the report with feature-usage columns read from
43
+ // Enrich the report with feature-usage columns read from
44
44
  // the Frigg-owned usage store. Best-effort — a failure to construct it never
45
45
  // blocks the structural report.
46
46
  let usageRepository = null;
@@ -26,7 +26,7 @@ class ListIntegrationsReport {
26
26
  throw new Error('reportingRepository is required');
27
27
  }
28
28
  this.reportingRepository = reportingRepository;
29
- // Optional ADR-011 usage store (the ADR-010 hand-off). When present, the
29
+ // Optional usage store (the ADR-010 hand-off). When present, the
30
30
  // report's byType buckets are enriched with feature-usage columns read
31
31
  // ONLY from this store — never an external APM.
32
32
  this.usageRepository = usageRepository || null;
@@ -87,7 +87,7 @@ class ListIntegrationsReport {
87
87
  bucket.byStatus[statusKey] = (bucket.byStatus[statusKey] ?? 0) + 1;
88
88
  }
89
89
 
90
- // Additive usage columns (ADR-011). Read per-type totals for each
90
+ // Additive usage columns. Read per-type totals for each
91
91
  // canonical counter; a type with no rows reads as 0. Guarded so a usage
92
92
  // store failure never breaks the structural report.
93
93
  if (this.usageRepository) {
@@ -118,7 +118,7 @@ class ListIntegrationsReport {
118
118
  try {
119
119
  const totalsByMetric = await Promise.all(
120
120
  metrics.map(async (metric) => {
121
- const totals = await this.usageRepository.totals({
121
+ const totals = await this.usageRepository.getTotalsByDimension({
122
122
  metric,
123
123
  groupBy: 'integrationType',
124
124
  });
@@ -55,6 +55,15 @@ const Definition = {
55
55
  defaults to `none` (no per-event cost, no data written to CloudWatch). Point
56
56
  `exporter` at an OTLP backend to turn export on.
57
57
 
58
+ **Sampling (`sampleRatio`, `0..1`, default `1`):** the fraction of **traces**
59
+ exported — a cost knob for high-traffic fleets (`0.1` ≈ keep 10%). Whole traces
60
+ are sampled (trace-ID-based + parent-based, so a distributed trace is never
61
+ half-kept), and it does **not** thin the durable **usage counters** — those stay
62
+ exact at any ratio (they ride the event bus, not the sampled trace pipeline). It
63
+ is **not** error-aware: a low ratio drops failed-run traces too, so for "keep all
64
+ errors, sample the rest" use tail-based sampling at an OTel Collector, not this
65
+ knob. Typical: `1.0` in dev, lower (e.g. `0.1`) in high-volume prod.
66
+
58
67
  ### Environment variables
59
68
 
60
69
  | Variable | Purpose |
@@ -154,7 +163,7 @@ const { createFriggCommands } = require('@friggframework/core');
154
163
  const frigg = createFriggCommands({ integrationClass: HubSpotIntegration });
155
164
 
156
165
  // Apples-to-apples comparison across integration types:
157
- await frigg.usage.totals({
166
+ await frigg.usage.getTotalsByDimension({
158
167
  metric: 'records.synced',
159
168
  groupBy: 'integrationType', // or 'metric'
160
169
  since: daysAgo(30),
@@ -163,7 +172,7 @@ await frigg.usage.totals({
163
172
  // → [{ integrationType: 'hubspot', value: 4200 }, { integrationType: 'salesforce', value: 1180 }]
164
173
 
165
174
  // Trend series for one type (aggregated across its instances):
166
- await frigg.usage.series({
175
+ await frigg.usage.getTimeSeries({
167
176
  metric: 'records.synced',
168
177
  integrationType: 'hubspot',
169
178
  from: daysAgo(7),
@@ -211,19 +220,24 @@ Read it as a first-class metric without knowing the configured key — the North
211
220
  Star resolves per integration type (`byType` wins over `default`):
212
221
 
213
222
  ```js
214
- // Resolves the configured counter for the type, then returns its totals.
215
- await frigg.usage.northStar({ integrationType: 'hubspot', since: daysAgo(30) });
223
+ // The caller passes the North Star config it already holds (from the app
224
+ // definition); this resolves the counter for the type and returns its totals.
225
+ await frigg.usage.getNorthStarTotals({
226
+ northStar: definition.telemetry.northStar,
227
+ integrationType: 'hubspot',
228
+ since: daysAgo(30),
229
+ });
216
230
  // → { metric: 'contacts_synced', totals: [{ integrationType: 'hubspot', value: 900 }] }
217
- // → null when no North Star is configured (caller branches without knowing keys)
231
+ // → null when `northStar` is absent or has no entry for the type
218
232
  ```
219
233
 
220
234
  Or read it like any counter once you know the key:
221
- `frigg.usage.totals({ metric: 'contacts_synced' })`; trends via `frigg.usage.series({ metric })`.
235
+ `frigg.usage.getTotalsByDimension({ metric: 'contacts_synced' })`; trends via `frigg.usage.getTimeSeries({ metric })`.
222
236
 
223
- > `northStar` config is injected at the composition root — the caller that owns
224
- > the app definition passes it in: `createFriggCommands({ integrationClass, northStar })`
225
- > (the application layer never reaches up to load it). Omitted → `northStar()`
226
- > returns `null`.
237
+ > The North Star read takes its config as a **call argument** — nothing
238
+ > telemetry-specific is threaded through `createFriggCommands`. The caller that
239
+ > owns the app definition (e.g. a report runner) passes `telemetry.northStar` in;
240
+ > omit it and `getNorthStarTotals()` returns `null`.
227
241
 
228
242
  ## Plugin / extension tap
229
243
 
@@ -286,7 +300,7 @@ this.telemetry.count / auto-instrumented seam
286
300
  │ (discards on SQS redelivery — approximate contract)
287
301
  └─ your plugin taps
288
302
 
289
- frigg.usage.totals / series ◄── UsageCounter store ──► reporting usage columns
303
+ frigg.usage.getTotalsByDimension / getTimeSeries ◄── UsageCounter store ──► reporting usage columns
290
304
  ```
291
305
 
292
306
  - **Flush is Lambda-safe:** `create-handler` awaits a bounded `forceFlush` in a
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Wrap a telemetry service so an integration instance's emissions automatically
3
- * carry its `integration_type` (ADR-011). This lets developers write
3
+ * carry its `integration_type`. This lets developers write
4
4
  * `this.telemetry.count('records.synced', n, { entity })` with no per-call
5
5
  * boilerplate, while the usage rollup still attributes the counter to the right
6
6
  * integration type.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Canonical usage-counter vocabulary (ADR-011 Usage-Counter Contract §1).
2
+ * Canonical usage-counter vocabulary.
3
3
  *
4
4
  * These core-owned, versioned keys are the ONLY metrics guaranteed comparable
5
5
  * *across* integration types — they power ADR-010's apples-to-apples comparison
@@ -0,0 +1,19 @@
1
+ const {
2
+ TelemetryExporterInterface,
3
+ } = require('./telemetry-exporter-interface');
4
+
5
+ /** Console span/metric exporters — local-dev visibility, no network. */
6
+ class ConsoleExporter extends TelemetryExporterInterface {
7
+ build() {
8
+ const {
9
+ ConsoleSpanExporter,
10
+ } = require('@opentelemetry/sdk-trace-base');
11
+ const { ConsoleMetricExporter } = require('@opentelemetry/sdk-metrics');
12
+ return {
13
+ traceExporter: new ConsoleSpanExporter(),
14
+ metricExporter: new ConsoleMetricExporter(),
15
+ };
16
+ }
17
+ }
18
+
19
+ module.exports = { ConsoleExporter };
@@ -0,0 +1,11 @@
1
+ const { OtlpExporter } = require('./otlp-exporter');
2
+
3
+ /**
4
+ * Datadog ingests OTLP/HTTP directly, so today it behaves exactly like
5
+ * `OtlpExporter` (endpoint + headers). It is its own class so Datadog-specific
6
+ * presets (e.g. a default `DD-API-KEY` header) can be added later without
7
+ * touching the OTLP exporter or the registry.
8
+ */
9
+ class DatadogExporter extends OtlpExporter {}
10
+
11
+ module.exports = { DatadogExporter };
@@ -0,0 +1,13 @@
1
+ const { OtlpExporter } = require('./otlp-exporter');
2
+
3
+ /** Honeycomb: an OTLP preset — default endpoint + `x-honeycomb-team` from apiKey. */
4
+ class HoneycombExporter extends OtlpExporter {
5
+ constructor({ endpoint, apiKey, headers } = {}) {
6
+ super({
7
+ endpoint: endpoint || 'https://api.honeycomb.io',
8
+ headers: apiKey ? { 'x-honeycomb-team': apiKey } : headers,
9
+ });
10
+ }
11
+ }
12
+
13
+ module.exports = { HoneycombExporter };
@@ -0,0 +1,42 @@
1
+ const {
2
+ TelemetryExporterInterface,
3
+ } = require('./telemetry-exporter-interface');
4
+
5
+ /**
6
+ * OTLP/HTTP exporter to a configured endpoint (+ optional headers). With no
7
+ * endpoint the OTel SDK falls back to the standard `OTEL_EXPORTER_OTLP_ENDPOINT`
8
+ * env var, so `url` is left unset in that case.
9
+ */
10
+ class OtlpExporter extends TelemetryExporterInterface {
11
+ constructor({ endpoint, headers } = {}) {
12
+ super();
13
+ this.endpoint = endpoint;
14
+ this.headers = headers;
15
+ }
16
+
17
+ build() {
18
+ const {
19
+ OTLPTraceExporter,
20
+ } = require('@opentelemetry/exporter-trace-otlp-http');
21
+ const {
22
+ OTLPMetricExporter,
23
+ } = require('@opentelemetry/exporter-metrics-otlp-http');
24
+ return {
25
+ traceExporter: new OTLPTraceExporter(this._options('/v1/traces')),
26
+ metricExporter: new OTLPMetricExporter(this._options('/v1/metrics')),
27
+ };
28
+ }
29
+
30
+ _options(path) {
31
+ return {
32
+ ...(this.endpoint ? { url: joinPath(this.endpoint, path) } : {}),
33
+ ...(this.headers ? { headers: this.headers } : {}),
34
+ };
35
+ }
36
+ }
37
+
38
+ function joinPath(base, path) {
39
+ return `${String(base).replace(/\/$/, '')}${path}`;
40
+ }
41
+
42
+ module.exports = { OtlpExporter };
@@ -0,0 +1,25 @@
1
+ const {
2
+ TelemetryExporterInterface,
3
+ } = require('./telemetry-exporter-interface');
4
+
5
+ /**
6
+ * Adopter/test-supplied exporter instances, used as-is (e.g. in-memory exporters
7
+ * in tests, or an advanced adopter wiring its own OTel exporter). Pre-built
8
+ * instances win over a `type`.
9
+ */
10
+ class PassthroughExporter extends TelemetryExporterInterface {
11
+ constructor({ traceExporter = null, metricExporter = null } = {}) {
12
+ super();
13
+ this.traceExporter = traceExporter;
14
+ this.metricExporter = metricExporter;
15
+ }
16
+
17
+ build() {
18
+ return {
19
+ traceExporter: this.traceExporter,
20
+ metricExporter: this.metricExporter,
21
+ };
22
+ }
23
+ }
24
+
25
+ module.exports = { PassthroughExporter };
@@ -0,0 +1,39 @@
1
+ const { PassthroughExporter } = require('./passthrough-exporter');
2
+ const { ConsoleExporter } = require('./console-exporter');
3
+ const { OtlpExporter } = require('./otlp-exporter');
4
+ const { HoneycombExporter } = require('./honeycomb-exporter');
5
+ const { DatadogExporter } = require('./datadog-exporter');
6
+
7
+ // type → exporter adapter. Add a destination by adding a class + one entry.
8
+ const EXPORTERS = {
9
+ console: ConsoleExporter,
10
+ otlp: OtlpExporter,
11
+ datadog: DatadogExporter,
12
+ honeycomb: HoneycombExporter,
13
+ };
14
+
15
+ /**
16
+ * Resolve the exporter adapter for a telemetry descriptor. Pre-built exporter
17
+ * instances (tests / advanced adopters) win over `type`; an unknown or absent
18
+ * type falls back to plain OTLP (which itself falls back to the standard OTLP
19
+ * env var). Replaces the former switch-based factory with polymorphic adapters.
20
+ *
21
+ * @returns {import('./telemetry-exporter-interface').TelemetryExporterInterface}
22
+ */
23
+ function resolveExporter(descriptor = {}) {
24
+ if (descriptor.traceExporter || descriptor.metricExporter) {
25
+ return new PassthroughExporter(descriptor);
26
+ }
27
+ // Own-property check so a `type` matching an Object.prototype member
28
+ // ('constructor', 'toString', …) can't resolve an inherited key — unknown
29
+ // types must fall through to plain OTLP.
30
+ const Exporter = Object.prototype.hasOwnProperty.call(
31
+ EXPORTERS,
32
+ descriptor.type
33
+ )
34
+ ? EXPORTERS[descriptor.type]
35
+ : OtlpExporter;
36
+ return new Exporter(descriptor);
37
+ }
38
+
39
+ module.exports = { resolveExporter, EXPORTERS };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Port for a telemetry exporter: builds the OTel trace + metric exporter
3
+ * instances for one destination. Adapters (console, otlp, honeycomb, datadog, a
4
+ * passthrough for pre-built instances) implement `build()`. OpenTelemetry SDK
5
+ * packages are `require`d lazily inside `build()` so merely loading an adapter
6
+ * pulls in no OTel modules.
7
+ */
8
+ class TelemetryExporterInterface {
9
+ /** @returns {{ traceExporter: object|null, metricExporter: object|null }} */
10
+ build() {
11
+ throw new Error(
12
+ 'build() must be implemented by a TelemetryExporter subclass'
13
+ );
14
+ }
15
+ }
16
+
17
+ module.exports = { TelemetryExporterInterface };
@@ -1,12 +1,15 @@
1
1
  const { createTelemetry, isNoOpExporter } = require('./telemetry-service');
2
- const { createNoOpTelemetry } = require('./no-op-telemetry');
2
+ const {
3
+ TelemetryServiceInterface,
4
+ } = require('./telemetry-service-interface');
5
+ const { NoOpTelemetry } = require('./no-op-telemetry');
3
6
  const { createTelemetryEventBus } = require('./telemetry-event-bus');
4
7
  const { resolveTelemetryConfig } = require('./telemetry-config');
5
8
  const {
6
9
  getTelemetry,
7
10
  resetTelemetryForTests,
8
11
  } = require('./telemetry-singleton');
9
- const { buildExporters } = require('./exporters/exporter-factory');
12
+ const { resolveExporter } = require('./exporters/resolve-exporter');
10
13
  const { instrumentHandler } = require('./instrument-handler');
11
14
  const { bindTelemetryContext } = require('./bind-telemetry-context');
12
15
  const { createUsageRollupSubscriber } = require('./usage-rollup-subscriber');
@@ -32,13 +35,14 @@ const {
32
35
 
33
36
  module.exports = {
34
37
  createTelemetry,
35
- createNoOpTelemetry,
38
+ TelemetryServiceInterface,
39
+ NoOpTelemetry,
36
40
  createTelemetryEventBus,
37
41
  resolveTelemetryConfig,
38
42
  getTelemetry,
39
43
  resetTelemetryForTests,
40
44
  isNoOpExporter,
41
- buildExporters,
45
+ resolveExporter,
42
46
  instrumentHandler,
43
47
  bindTelemetryContext,
44
48
  createUsageRollupSubscriber,
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Wraps a single integration handler invocation with a span + the
3
- * `frigg.handler.invocations` counter (ADR-011 Decision 2). Shared by the two
3
+ * `frigg.handler.invocations` counter. Shared by the two
4
4
  * dispatch seams — `IntegrationBase.send()` and `IntegrationEventDispatcher` —
5
5
  * so both paths are instrumented identically.
6
6
  *