@newrelic/preflight 1.0.4 → 1.0.5
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 +2 -1
- package/dist/alerts/types.d.ts.map +1 -1
- package/dist/alerts/types.js.map +1 -1
- package/dist/config.js +3 -3
- package/dist/config.js.map +1 -1
- package/dist/dashboard/dashboard-server.d.ts +4 -0
- package/dist/dashboard/dashboard-server.d.ts.map +1 -1
- package/dist/dashboard/dashboard-server.js +42 -1
- package/dist/dashboard/dashboard-server.js.map +1 -1
- package/dist/dashboard/live-event-bus.js +2 -2
- package/dist/dashboard/live-event-bus.js.map +1 -1
- package/dist/dashboard/routes/sse-handler.js +3 -3
- package/dist/dashboard/routes/sse-handler.js.map +1 -1
- package/dist/hooks/collector-script.d.ts.map +1 -1
- package/dist/hooks/collector-script.js +1 -5
- package/dist/hooks/collector-script.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/install/schedule.d.ts +1 -0
- package/dist/install/schedule.d.ts.map +1 -1
- package/dist/install/schedule.js +23 -5
- package/dist/install/schedule.js.map +1 -1
- package/dist/metrics/anti-patterns.d.ts.map +1 -1
- package/dist/metrics/anti-patterns.js.map +1 -1
- package/dist/metrics/context-composition-tracker.d.ts.map +1 -1
- package/dist/metrics/context-composition-tracker.js.map +1 -1
- package/dist/platforms/copilot-adapter.d.ts.map +1 -1
- package/dist/platforms/copilot-adapter.js.map +1 -1
- package/dist/proxy/otlp-receiver.d.ts.map +1 -1
- package/dist/proxy/otlp-receiver.js.map +1 -1
- package/dist/proxy/proxy-manager.js +1 -1
- package/dist/proxy/proxy-manager.js.map +1 -1
- package/dist/proxy/upstream-stdio.d.ts.map +1 -1
- package/dist/proxy/upstream-stdio.js.map +1 -1
- package/dist/security/ssrf.js +1 -1
- package/dist/security/ssrf.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/shared/__test-utils__/log-output.d.ts +1 -2
- package/dist/shared/__test-utils__/log-output.d.ts.map +1 -1
- package/dist/shared/__test-utils__/log-output.js +1 -2
- package/dist/shared/__test-utils__/log-output.js.map +1 -1
- package/dist/shared/config.d.ts +8 -2
- package/dist/shared/config.d.ts.map +1 -1
- package/dist/shared/config.js +20 -20
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/errors.d.ts +1 -1
- package/dist/shared/errors.js +10 -10
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/events/factory.js +12 -12
- package/dist/shared/events/factory.js.map +1 -1
- package/dist/shared/events/serialize.d.ts +2 -2
- package/dist/shared/events/serialize.js +19 -19
- package/dist/shared/events/serialize.js.map +1 -1
- package/dist/shared/harvest/event-buffer.d.ts +2 -2
- package/dist/shared/harvest/event-buffer.js +3 -3
- package/dist/shared/harvest/event-buffer.js.map +1 -1
- package/dist/shared/harvest/harvest-scheduler.d.ts +4 -5
- package/dist/shared/harvest/harvest-scheduler.d.ts.map +1 -1
- package/dist/shared/harvest/harvest-scheduler.js +35 -35
- package/dist/shared/harvest/harvest-scheduler.js.map +1 -1
- package/dist/shared/harvest/metric-aggregator.d.ts +9 -9
- package/dist/shared/harvest/metric-aggregator.d.ts.map +1 -1
- package/dist/shared/harvest/metric-aggregator.js +16 -18
- package/dist/shared/harvest/metric-aggregator.js.map +1 -1
- package/dist/shared/index.d.ts +0 -1
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +0 -1
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/logger.d.ts +4 -5
- package/dist/shared/logger.d.ts.map +1 -1
- package/dist/shared/logger.js +12 -12
- package/dist/shared/logger.js.map +1 -1
- package/dist/shared/pricing-data.js +10 -10
- package/dist/shared/pricing-data.js.map +1 -1
- package/dist/shared/pricing.d.ts +3 -3
- package/dist/shared/pricing.js +12 -12
- package/dist/shared/pricing.js.map +1 -1
- package/dist/shared/redact.d.ts +1 -3
- package/dist/shared/redact.d.ts.map +1 -1
- package/dist/shared/redact.js +8 -10
- package/dist/shared/redact.js.map +1 -1
- package/dist/shared/timing.d.ts +4 -4
- package/dist/shared/timing.d.ts.map +1 -1
- package/dist/shared/timing.js +9 -9
- package/dist/shared/timing.js.map +1 -1
- package/dist/shared/tokens.d.ts +4 -4
- package/dist/shared/tokens.js +16 -16
- package/dist/shared/tokens.js.map +1 -1
- package/dist/shared/transport/events-api.d.ts +1 -1
- package/dist/shared/transport/events-api.d.ts.map +1 -1
- package/dist/shared/transport/events-api.js +2 -1
- package/dist/shared/transport/events-api.js.map +1 -1
- package/dist/shared/transport/http-client.d.ts +1 -1
- package/dist/shared/transport/http-client.d.ts.map +1 -1
- package/dist/shared/transport/http-client.js +19 -28
- package/dist/shared/transport/http-client.js.map +1 -1
- package/dist/shared/transport/logs-api.d.ts +2 -3
- package/dist/shared/transport/logs-api.d.ts.map +1 -1
- package/dist/shared/transport/logs-api.js +3 -3
- package/dist/shared/transport/logs-api.js.map +1 -1
- package/dist/shared/transport/metric-api.d.ts.map +1 -1
- package/dist/shared/transport/metric-api.js +1 -0
- package/dist/shared/transport/metric-api.js.map +1 -1
- package/dist/shared/transport/otlp-event-bridge.d.ts +10 -5
- package/dist/shared/transport/otlp-event-bridge.d.ts.map +1 -1
- package/dist/shared/transport/otlp-event-bridge.js +20 -11
- package/dist/shared/transport/otlp-event-bridge.js.map +1 -1
- package/dist/shared/transport/otlp-shared.d.ts +12 -1
- package/dist/shared/transport/otlp-shared.d.ts.map +1 -1
- package/dist/shared/transport/otlp-shared.js +20 -4
- package/dist/shared/transport/otlp-shared.js.map +1 -1
- package/dist/shared/transport/otlp-transport.d.ts +22 -19
- package/dist/shared/transport/otlp-transport.d.ts.map +1 -1
- package/dist/shared/transport/otlp-transport.js +136 -120
- package/dist/shared/transport/otlp-transport.js.map +1 -1
- package/dist/shared/transport/types.d.ts +7 -3
- package/dist/shared/transport/types.d.ts.map +1 -1
- package/dist/storage/session-store.js +1 -1
- package/dist/storage/weekly-summary.js +3 -3
- package/dist/storage/weekly-summary.js.map +1 -1
- package/dist/tools/cross-session-tools.js +1 -1
- package/dist/tools/cross-session-tools.js.map +1 -1
- package/dist/tools/session-stats.d.ts.map +1 -1
- package/dist/tools/session-stats.js +2 -1
- package/dist/tools/session-stats.js.map +1 -1
- package/dist/tracing/mcp-tracer.js +1 -1
- package/dist/tracing/mcp-tracer.js.map +1 -1
- package/dist/transport/nr-ingest.d.ts.map +1 -1
- package/dist/transport/nr-ingest.js +4 -0
- package/dist/transport/nr-ingest.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +21 -0
- package/dist/version.js.map +1 -0
- package/dist/web/assets/index-CW0UCwb9.css +2 -0
- package/dist/web/assets/index-HRyb4aZK.js +64 -0
- package/dist/web/index.html +2 -2
- package/package.json +23 -23
- package/dist/shared/version.d.ts +0 -2
- package/dist/shared/version.d.ts.map +0 -1
- package/dist/shared/version.js +0 -2
- package/dist/shared/version.js.map +0 -1
- package/dist/web/assets/index-BrL281N-.css +0 -2
- package/dist/web/assets/index-CcaYZzXm.js +0 -42
|
@@ -2,31 +2,40 @@ import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
|
|
|
2
2
|
import { LoggerProvider, BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
|
|
3
3
|
import { resourceFromAttributes } from '@opentelemetry/resources';
|
|
4
4
|
import { createLogger } from '../logger.js';
|
|
5
|
-
import { validateOtlpEndpoint, hasOtlpAuthHeader, DEFAULT_CLIENT_NAME } from './otlp-shared.js';
|
|
5
|
+
import { validateOtlpEndpoint, hasOtlpAuthHeader, DEFAULT_CLIENT_NAME, buildUserAgent, sanitizeClientString, } from './otlp-shared.js';
|
|
6
6
|
const logger = createLogger('otlp-event-bridge');
|
|
7
7
|
export class OtlpEventBridge {
|
|
8
8
|
loggerProvider;
|
|
9
9
|
otelLogger;
|
|
10
|
+
hasAuth;
|
|
11
|
+
endpoint;
|
|
12
|
+
hasWarnedNoAuth = false;
|
|
10
13
|
constructor(options) {
|
|
11
14
|
validateOtlpEndpoint(options.endpoint, 'OtlpEventBridge');
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
endpoint: options.endpoint,
|
|
17
|
-
});
|
|
18
|
-
}
|
|
15
|
+
const clientName = sanitizeClientString(options.clientName, DEFAULT_CLIENT_NAME);
|
|
16
|
+
const clientVersion = sanitizeClientString(options.clientVersion, '');
|
|
17
|
+
this.endpoint = options.endpoint;
|
|
18
|
+
this.hasAuth = hasOtlpAuthHeader(options.headers ?? {});
|
|
19
19
|
const exporter = new OTLPLogExporter({
|
|
20
20
|
url: `${options.endpoint}/v1/logs`,
|
|
21
|
-
headers:
|
|
21
|
+
headers: {
|
|
22
|
+
...(options.headers ?? {}),
|
|
23
|
+
'User-Agent': buildUserAgent(clientName, clientVersion),
|
|
24
|
+
},
|
|
22
25
|
});
|
|
23
26
|
this.loggerProvider = new LoggerProvider({
|
|
24
27
|
resource: resourceFromAttributes({ 'service.name': options.appName }),
|
|
25
28
|
processors: [new BatchLogRecordProcessor(exporter)],
|
|
26
29
|
});
|
|
27
|
-
this.otelLogger = this.loggerProvider.getLogger(
|
|
30
|
+
this.otelLogger = this.loggerProvider.getLogger(clientName, clientVersion || undefined);
|
|
28
31
|
}
|
|
29
32
|
sendEvents(events) {
|
|
33
|
+
if (!this.hasAuth && !this.hasWarnedNoAuth) {
|
|
34
|
+
this.hasWarnedNoAuth = true;
|
|
35
|
+
logger.warn('OtlpEventBridge sending events with no auth header — collector may reject', {
|
|
36
|
+
endpoint: this.endpoint,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
30
39
|
for (const event of events) {
|
|
31
40
|
this.otelLogger.emit({
|
|
32
41
|
severityText: 'INFO',
|
|
@@ -34,7 +43,7 @@ export class OtlpEventBridge {
|
|
|
34
43
|
// Filter to scalar values only — the OTel SDK's AnyValue type also
|
|
35
44
|
// accepts arrays/objects/null, and a non-scalar value would produce a
|
|
36
45
|
// malformed log record. NrEventData is typed as all-scalar but callers
|
|
37
|
-
// may pass unexpected shapes
|
|
46
|
+
// may pass unexpected shapes.
|
|
38
47
|
attributes: Object.fromEntries(Object.entries(event).filter(([, v]) => typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')),
|
|
39
48
|
timestamp: typeof event.timestamp === 'number' ? event.timestamp : Date.now(),
|
|
40
49
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"otlp-event-bridge.js","sourceRoot":"","sources":["../../../src/shared/transport/otlp-event-bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AACzE,OAAO,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClF,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"otlp-event-bridge.js","sourceRoot":"","sources":["../../../src/shared/transport/otlp-event-bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,10 +4,21 @@
|
|
|
4
4
|
* import this constant rather than repeating the literal.
|
|
5
5
|
*/
|
|
6
6
|
export declare const DEFAULT_CLIENT_NAME = "ai-telemetry";
|
|
7
|
+
/**
|
|
8
|
+
* Strip HTTP header-injection characters and surrounding whitespace from a
|
|
9
|
+
* client name or version string. Falls back to `fallback` when the input is
|
|
10
|
+
* absent or reduces to an empty string after sanitization.
|
|
11
|
+
*/
|
|
12
|
+
export declare function sanitizeClientString(s: string | undefined, fallback: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Builds the `User-Agent` header value for outbound NR ingest requests.
|
|
15
|
+
* Returns `name/version` when version is non-empty, `name` alone otherwise.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildUserAgent(clientName: string | undefined, clientVersion: string | undefined): string;
|
|
7
18
|
/**
|
|
8
19
|
* Returns true when `headers` contains at least one recognised auth header
|
|
9
20
|
* (`api-key`, `authorization`, or `x-license-key`). Shared by OtlpTransport
|
|
10
|
-
* and OtlpEventBridge so both warn when no auth header is present
|
|
21
|
+
* and OtlpEventBridge so both warn when no auth header is present.
|
|
11
22
|
*/
|
|
12
23
|
export declare function hasOtlpAuthHeader(headers: Record<string, string>): boolean;
|
|
13
24
|
export declare function validateOtlpEndpoint(endpoint: string, source: string): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"otlp-shared.d.ts","sourceRoot":"","sources":["../../../src/shared/transport/otlp-shared.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAElD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAM1E;AAOD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAsB3E"}
|
|
1
|
+
{"version":3,"file":"otlp-shared.d.ts","sourceRoot":"","sources":["../../../src/shared/transport/otlp-shared.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAElD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEpF;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,aAAa,EAAE,MAAM,GAAG,SAAS,GAChC,MAAM,CAGR;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAM1E;AAOD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAsB3E"}
|
|
@@ -6,10 +6,26 @@ const logger = createLogger('otlp-shared');
|
|
|
6
6
|
* import this constant rather than repeating the literal.
|
|
7
7
|
*/
|
|
8
8
|
export const DEFAULT_CLIENT_NAME = 'ai-telemetry';
|
|
9
|
+
/**
|
|
10
|
+
* Strip HTTP header-injection characters and surrounding whitespace from a
|
|
11
|
+
* client name or version string. Falls back to `fallback` when the input is
|
|
12
|
+
* absent or reduces to an empty string after sanitization.
|
|
13
|
+
*/
|
|
14
|
+
export function sanitizeClientString(s, fallback) {
|
|
15
|
+
return (s ?? fallback).replace(/[\r\n\x00-\x1f]/g, '').trim() || fallback;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Builds the `User-Agent` header value for outbound NR ingest requests.
|
|
19
|
+
* Returns `name/version` when version is non-empty, `name` alone otherwise.
|
|
20
|
+
*/
|
|
21
|
+
export function buildUserAgent(clientName, clientVersion) {
|
|
22
|
+
const name = clientName || DEFAULT_CLIENT_NAME;
|
|
23
|
+
return clientVersion ? `${name}/${clientVersion}` : name;
|
|
24
|
+
}
|
|
9
25
|
/**
|
|
10
26
|
* Returns true when `headers` contains at least one recognised auth header
|
|
11
27
|
* (`api-key`, `authorization`, or `x-license-key`). Shared by OtlpTransport
|
|
12
|
-
* and OtlpEventBridge so both warn when no auth header is present
|
|
28
|
+
* and OtlpEventBridge so both warn when no auth header is present.
|
|
13
29
|
*/
|
|
14
30
|
export function hasOtlpAuthHeader(headers) {
|
|
15
31
|
for (const key of Object.keys(headers)) {
|
|
@@ -19,11 +35,11 @@ export function hasOtlpAuthHeader(headers) {
|
|
|
19
35
|
}
|
|
20
36
|
return false;
|
|
21
37
|
}
|
|
22
|
-
//
|
|
38
|
+
// Validate OTLP endpoint scheme. https:// is required for
|
|
23
39
|
// any non-localhost destination because the payload may contain user prompt
|
|
24
40
|
// fragments (PII). Plain http:// is allowed only against loopback to support
|
|
25
41
|
// local development and testing. Shared by OtlpTransport and OtlpEventBridge
|
|
26
|
-
// (
|
|
42
|
+
// (deduplicates previously identical implementations).
|
|
27
43
|
export function validateOtlpEndpoint(endpoint, source) {
|
|
28
44
|
let parsed;
|
|
29
45
|
try {
|
|
@@ -40,7 +56,7 @@ export function validateOtlpEndpoint(endpoint, source) {
|
|
|
40
56
|
// http://: only acceptable on loopback
|
|
41
57
|
const host = parsed.hostname;
|
|
42
58
|
// 0.0.0.0 is a wildcard that binds to ALL interfaces, not loopback only —
|
|
43
|
-
// cleartext traffic to it is reachable from any network
|
|
59
|
+
// cleartext traffic to it is reachable from any network.
|
|
44
60
|
const isLoopback = host === 'localhost' || host === '127.0.0.1' || host === '::1';
|
|
45
61
|
if (!isLoopback) {
|
|
46
62
|
logger.warn(`${source}: OTLP endpoint uses plain http:// to a non-loopback host — payload may contain PII and should not be transmitted in cleartext`, { endpoint });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"otlp-shared.js","sourceRoot":"","sources":["../../../src/shared/transport/otlp-shared.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,MAAM,MAAM,GAAG,YAAY,CAAC,aAAa,CAAC,CAAC;AAE3C;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,cAAc,CAAC;AAElD;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAA+B;IAC/D,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,MAAM,EAAE,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;QAC7B,IAAI,EAAE,KAAK,SAAS,IAAI,EAAE,KAAK,eAAe,IAAI,EAAE,KAAK,eAAe;YAAE,OAAO,IAAI,CAAC;IACxF,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,0DAA0D;AAC1D,4EAA4E;AAC5E,6EAA6E;AAC7E,6EAA6E;AAC7E,
|
|
1
|
+
{"version":3,"file":"otlp-shared.js","sourceRoot":"","sources":["../../../src/shared/transport/otlp-shared.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,MAAM,MAAM,GAAG,YAAY,CAAC,aAAa,CAAC,CAAC;AAE3C;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,cAAc,CAAC;AAElD;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,CAAqB,EAAE,QAAgB;IAC1E,OAAO,CAAC,CAAC,IAAI,QAAQ,CAAC,CAAC,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,QAAQ,CAAC;AAC5E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC5B,UAA8B,EAC9B,aAAiC;IAEjC,MAAM,IAAI,GAAG,UAAU,IAAI,mBAAmB,CAAC;IAC/C,OAAO,aAAa,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,aAAa,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC3D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAA+B;IAC/D,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,MAAM,EAAE,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;QAC7B,IAAI,EAAE,KAAK,SAAS,IAAI,EAAE,KAAK,eAAe,IAAI,EAAE,KAAK,eAAe;YAAE,OAAO,IAAI,CAAC;IACxF,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,0DAA0D;AAC1D,4EAA4E;AAC5E,6EAA6E;AAC7E,6EAA6E;AAC7E,uDAAuD;AACvD,MAAM,UAAU,oBAAoB,CAAC,QAAgB,EAAE,MAAc;IACnE,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,GAAG,MAAM,gCAAgC,QAAQ,EAAE,CAAC,CAAC;IACvE,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ;QAAE,OAAO;IACzC,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,GAAG,MAAM,yCAAyC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IACvF,CAAC;IACD,uCAAuC;IACvC,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC;IAC7B,0EAA0E;IAC1E,yDAAyD;IACzD,MAAM,UAAU,GAAG,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,KAAK,CAAC;IAClF,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,CAAC,IAAI,CACT,GAAG,MAAM,gIAAgI,EACzI,EAAE,QAAQ,EAAE,CACb,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import type { NrMetric } from './types.js';
|
|
2
2
|
export interface OtlpTransportOptions {
|
|
3
|
-
endpoint: string;
|
|
4
|
-
headers?: Record<string, string>;
|
|
5
|
-
appName: string;
|
|
6
|
-
/** Override the default 30-second request timeout for exportMetrics
|
|
7
|
-
requestTimeoutMs?: number;
|
|
3
|
+
readonly endpoint: string;
|
|
4
|
+
readonly headers?: Record<string, string>;
|
|
5
|
+
readonly appName: string;
|
|
6
|
+
/** Override the default 30-second request timeout for exportMetrics. */
|
|
7
|
+
readonly requestTimeoutMs?: number;
|
|
8
8
|
/**
|
|
9
9
|
* Identifies the consuming client in the `User-Agent` header and as the
|
|
10
10
|
* OTel instrumentation scope name. Defaults to
|
|
11
|
-
* `'ai-telemetry'` when not provided. Pass `'preflight'`, `'
|
|
11
|
+
* `'ai-telemetry'` when not provided. Pass `'preflight'`, `'nr-ai-agent'`,
|
|
12
12
|
* etc. so telemetry from different consumers is distinguishable.
|
|
13
13
|
*/
|
|
14
|
-
clientName?: string;
|
|
14
|
+
readonly clientName?: string;
|
|
15
|
+
/** Version of the consuming client, stamped as the OTel instrumentation scope version. */
|
|
16
|
+
readonly clientVersion?: string;
|
|
15
17
|
}
|
|
16
18
|
export declare class OtlpTransport {
|
|
17
19
|
private readonly traceExporter;
|
|
@@ -21,24 +23,21 @@ export declare class OtlpTransport {
|
|
|
21
23
|
private readonly endpoint;
|
|
22
24
|
private readonly headers;
|
|
23
25
|
private readonly requestTimeoutMs;
|
|
24
|
-
private readonly
|
|
26
|
+
private readonly clientVersion;
|
|
27
|
+
private readonly userAgent;
|
|
28
|
+
private readonly hasAuth;
|
|
25
29
|
private hasWarnedNoAuth;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
* Keeping a single resolved attribute map prevents `service.name` from
|
|
30
|
-
* drifting between the two paths — previously `this.appName` was stored
|
|
31
|
-
* separately and re-encoded into the OTLP envelope, which would silently
|
|
32
|
-
* diverge if anyone added a second resource attribute.
|
|
33
|
-
*/
|
|
34
|
-
private readonly resourceAttributes;
|
|
30
|
+
private readonly otlpResource;
|
|
31
|
+
private readonly otlpScope;
|
|
32
|
+
private readonly metricsHeaders;
|
|
35
33
|
constructor(options: OtlpTransportOptions);
|
|
36
34
|
flush(): Promise<void>;
|
|
37
35
|
shutdown(): Promise<void>;
|
|
36
|
+
private settledOrThrow;
|
|
38
37
|
/**
|
|
39
38
|
* Return an OTel `Tracer` for the given instrumentation name. The returned
|
|
40
|
-
* value is the `Tracer` interface from `@opentelemetry/api
|
|
41
|
-
|
|
39
|
+
* value is the `Tracer` interface from `@opentelemetry/api`;
|
|
40
|
+
* consumers that bind to the type explicitly should
|
|
42
41
|
* `import type { Tracer } from '@opentelemetry/api'`. `@opentelemetry/api`
|
|
43
42
|
* is already a regular dependency of this package, so no extra install is
|
|
44
43
|
* required. The type is intentionally NOT re-exported from this package's
|
|
@@ -54,5 +53,9 @@ export declare class OtlpTransport {
|
|
|
54
53
|
*/
|
|
55
54
|
getMeter(name: string): import("@opentelemetry/api").Meter;
|
|
56
55
|
exportMetrics(metrics: NrMetric[]): Promise<void>;
|
|
56
|
+
private otlpAttributes;
|
|
57
|
+
private numericDataPoint;
|
|
58
|
+
private summaryDataPoint;
|
|
59
|
+
private otlpMetric;
|
|
57
60
|
}
|
|
58
61
|
//# sourceMappingURL=otlp-transport.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"otlp-transport.d.ts","sourceRoot":"","sources":["../../../src/shared/transport/otlp-transport.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"otlp-transport.d.ts","sourceRoot":"","sources":["../../../src/shared/transport/otlp-transport.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAiD,MAAM,YAAY,CAAC;AAa1F,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,wEAAwE;IACxE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,0FAA0F;IAC1F,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAoB;IAClD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IACpD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAsB;IACrD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IACjD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAClC,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAiE;IAC9F,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuD;IACjF,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAmC;gBAEtD,OAAO,EAAE,oBAAoB;IAuDnC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAOtB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;YAOjB,cAAc;IAS5B;;;;;;;;;;OAUG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM;IAItB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM;IAIf,aAAa,CAAC,OAAO,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IA8DvD,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,gBAAgB;IAsBxB,OAAO,CAAC,gBAAgB;IAyBxB,OAAO,CAAC,UAAU;CA0BnB"}
|
|
@@ -6,8 +6,7 @@ import { BasicTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trac
|
|
|
6
6
|
import { MeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
|
|
7
7
|
import { resourceFromAttributes } from '@opentelemetry/resources';
|
|
8
8
|
import { createLogger } from '../logger.js';
|
|
9
|
-
import { validateOtlpEndpoint, hasOtlpAuthHeader, DEFAULT_CLIENT_NAME } from './otlp-shared.js';
|
|
10
|
-
import { VERSION } from '../version.js';
|
|
9
|
+
import { validateOtlpEndpoint, hasOtlpAuthHeader, DEFAULT_CLIENT_NAME, buildUserAgent, sanitizeClientString, } from './otlp-shared.js';
|
|
11
10
|
const gzipAsync = promisify(gzip);
|
|
12
11
|
const logger = createLogger('otlp-transport');
|
|
13
12
|
export class OtlpTransport {
|
|
@@ -18,32 +17,45 @@ export class OtlpTransport {
|
|
|
18
17
|
endpoint;
|
|
19
18
|
headers;
|
|
20
19
|
requestTimeoutMs;
|
|
21
|
-
|
|
20
|
+
clientVersion;
|
|
21
|
+
userAgent;
|
|
22
|
+
hasAuth;
|
|
22
23
|
hasWarnedNoAuth = false;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* Keeping a single resolved attribute map prevents `service.name` from
|
|
27
|
-
* drifting between the two paths — previously `this.appName` was stored
|
|
28
|
-
* separately and re-encoded into the OTLP envelope, which would silently
|
|
29
|
-
* diverge if anyone added a second resource attribute.
|
|
30
|
-
*/
|
|
31
|
-
resourceAttributes;
|
|
24
|
+
otlpResource;
|
|
25
|
+
otlpScope;
|
|
26
|
+
metricsHeaders;
|
|
32
27
|
constructor(options) {
|
|
33
28
|
validateOtlpEndpoint(options.endpoint, 'OtlpTransport');
|
|
34
|
-
|
|
35
|
-
this.
|
|
36
|
-
|
|
29
|
+
const clientName = sanitizeClientString(options.clientName, DEFAULT_CLIENT_NAME);
|
|
30
|
+
this.clientVersion = sanitizeClientString(options.clientVersion, '');
|
|
31
|
+
this.userAgent = buildUserAgent(clientName, this.clientVersion);
|
|
32
|
+
const resourceAttributes = Object.freeze({ 'service.name': options.appName });
|
|
33
|
+
this.otlpResource = Object.entries(resourceAttributes).map(([key, value]) => ({
|
|
34
|
+
key,
|
|
35
|
+
value: { stringValue: value },
|
|
36
|
+
}));
|
|
37
|
+
this.otlpScope = this.clientVersion
|
|
38
|
+
? { name: clientName, version: this.clientVersion }
|
|
39
|
+
: { name: clientName };
|
|
40
|
+
const resource = resourceFromAttributes({ ...resourceAttributes });
|
|
37
41
|
this.endpoint = options.endpoint;
|
|
38
|
-
this.headers = options.headers ?? {};
|
|
42
|
+
this.headers = { ...(options.headers ?? {}) };
|
|
43
|
+
this.hasAuth = hasOtlpAuthHeader(this.headers);
|
|
39
44
|
this.requestTimeoutMs = options.requestTimeoutMs ?? 30_000;
|
|
45
|
+
this.metricsHeaders = Object.freeze({
|
|
46
|
+
...this.headers,
|
|
47
|
+
'Content-Type': 'application/json',
|
|
48
|
+
'Content-Encoding': 'gzip',
|
|
49
|
+
'User-Agent': this.userAgent,
|
|
50
|
+
});
|
|
51
|
+
const sdkHeaders = { ...this.headers, 'User-Agent': this.userAgent };
|
|
40
52
|
this.traceExporter = new OTLPTraceExporter({
|
|
41
53
|
url: `${options.endpoint}/v1/traces`,
|
|
42
|
-
headers:
|
|
54
|
+
headers: sdkHeaders,
|
|
43
55
|
});
|
|
44
56
|
this.metricExporter = new OTLPMetricExporter({
|
|
45
57
|
url: `${options.endpoint}/v1/metrics`,
|
|
46
|
-
headers:
|
|
58
|
+
headers: sdkHeaders,
|
|
47
59
|
});
|
|
48
60
|
this.tracerProvider = new BasicTracerProvider({
|
|
49
61
|
resource,
|
|
@@ -60,17 +72,25 @@ export class OtlpTransport {
|
|
|
60
72
|
});
|
|
61
73
|
}
|
|
62
74
|
async flush() {
|
|
63
|
-
await this.tracerProvider.forceFlush();
|
|
64
|
-
await this.meterProvider.forceFlush();
|
|
75
|
+
await this.settledOrThrow([this.tracerProvider.forceFlush(), this.meterProvider.forceFlush()], 'OTLP flush failed on multiple providers');
|
|
65
76
|
}
|
|
66
77
|
async shutdown() {
|
|
67
|
-
await this.tracerProvider.shutdown();
|
|
68
|
-
|
|
78
|
+
await this.settledOrThrow([this.tracerProvider.shutdown(), this.meterProvider.shutdown()], 'OTLP shutdown failed on multiple providers');
|
|
79
|
+
}
|
|
80
|
+
async settledOrThrow(ops, message) {
|
|
81
|
+
const results = await Promise.allSettled(ops);
|
|
82
|
+
const errors = results
|
|
83
|
+
.filter((r) => r.status === 'rejected')
|
|
84
|
+
.map((r) => (r.reason instanceof Error ? r.reason : new Error(String(r.reason))));
|
|
85
|
+
if (errors.length === 1)
|
|
86
|
+
throw errors[0];
|
|
87
|
+
if (errors.length > 1)
|
|
88
|
+
throw new AggregateError(errors, message);
|
|
69
89
|
}
|
|
70
90
|
/**
|
|
71
91
|
* Return an OTel `Tracer` for the given instrumentation name. The returned
|
|
72
|
-
* value is the `Tracer` interface from `@opentelemetry/api
|
|
73
|
-
|
|
92
|
+
* value is the `Tracer` interface from `@opentelemetry/api`;
|
|
93
|
+
* consumers that bind to the type explicitly should
|
|
74
94
|
* `import type { Tracer } from '@opentelemetry/api'`. `@opentelemetry/api`
|
|
75
95
|
* is already a regular dependency of this package, so no extra install is
|
|
76
96
|
* required. The type is intentionally NOT re-exported from this package's
|
|
@@ -79,7 +99,7 @@ export class OtlpTransport {
|
|
|
79
99
|
* unrelated imports.
|
|
80
100
|
*/
|
|
81
101
|
getTracer(name) {
|
|
82
|
-
return this.tracerProvider.getTracer(name);
|
|
102
|
+
return this.tracerProvider.getTracer(name, this.clientVersion || undefined);
|
|
83
103
|
}
|
|
84
104
|
/**
|
|
85
105
|
* Return an OTel `Meter` for the given instrumentation name. See
|
|
@@ -87,106 +107,27 @@ export class OtlpTransport {
|
|
|
87
107
|
* (`import type { Meter } from '@opentelemetry/api'`).
|
|
88
108
|
*/
|
|
89
109
|
getMeter(name) {
|
|
90
|
-
return this.meterProvider.getMeter(name);
|
|
110
|
+
return this.meterProvider.getMeter(name, this.clientVersion || undefined);
|
|
91
111
|
}
|
|
92
112
|
async exportMetrics(metrics) {
|
|
93
113
|
if (metrics.length === 0)
|
|
94
114
|
return;
|
|
95
115
|
// Warn ONCE when no auth header is present — emitting on every call would
|
|
96
|
-
// flood stderr with identical lines in long-running agents
|
|
97
|
-
if (!
|
|
116
|
+
// flood stderr with identical lines in long-running agents.
|
|
117
|
+
if (!this.hasAuth && !this.hasWarnedNoAuth) {
|
|
98
118
|
this.hasWarnedNoAuth = true;
|
|
99
119
|
logger.warn('OTLP metric export attempted with no auth header — collector may reject', {
|
|
100
120
|
endpoint: this.endpoint,
|
|
101
121
|
});
|
|
102
122
|
}
|
|
103
|
-
const otlpAttributes = (attrs) => Object.entries(attrs ?? {}).map(([key, value]) => ({
|
|
104
|
-
key,
|
|
105
|
-
value: typeof value === 'number'
|
|
106
|
-
? { doubleValue: value }
|
|
107
|
-
: typeof value === 'boolean'
|
|
108
|
-
? { boolValue: value }
|
|
109
|
-
: { stringValue: String(value) },
|
|
110
|
-
}));
|
|
111
|
-
const numericDataPoint = (m) => ({
|
|
112
|
-
// For count (delta Sum) metrics, include startTimeUnixNano per the OTLP
|
|
113
|
-
// spec (§TR2). Gauge data points do not require it but it is harmless.
|
|
114
|
-
// Clamp to 0: if timestamp < intervalMs (misconfigured metric), a negative
|
|
115
|
-
// startTimeUnixNano would be rejected by strict OTLP collectors (§TR7).
|
|
116
|
-
startTimeUnixNano: m.type === 'count' ? Math.max(0, m.timestamp - m.intervalMs) * 1_000_000 : undefined,
|
|
117
|
-
timeUnixNano: m.timestamp * 1_000_000,
|
|
118
|
-
asDouble: m.value,
|
|
119
|
-
attributes: otlpAttributes(m.attributes),
|
|
120
|
-
});
|
|
121
|
-
// summary is now a first-class type with a structured
|
|
122
|
-
// value `{ count, sum, min, max }`. OTLP doesn't have a single
|
|
123
|
-
// "Summary"-shaped metric kind; the closest faithful mapping is OTLP
|
|
124
|
-
// Histogram with explicit `count` and `sum` fields plus per-data-point
|
|
125
|
-
// `min` / `max`. Bucket boundaries are intentionally omitted: NR doesn't
|
|
126
|
-
// need them for summary aggregation, and emitting empty `bucketCounts`
|
|
127
|
-
// alongside `min`/`max` is the documented OTLP shape for unbucketed
|
|
128
|
-
// summaries (`explicitBounds: []`, `bucketCounts: [<count>]`).
|
|
129
|
-
const summaryDataPoint = (m) => ({
|
|
130
|
-
// OTLP Histogram with DELTA temporality requires startTimeUnixNano (§TR2).
|
|
131
|
-
startTimeUnixNano: Math.max(0, m.timestamp - m.intervalMs) * 1_000_000,
|
|
132
|
-
timeUnixNano: m.timestamp * 1_000_000,
|
|
133
|
-
attributes: otlpAttributes(m.attributes),
|
|
134
|
-
count: m.value.count,
|
|
135
|
-
sum: m.value.sum,
|
|
136
|
-
min: m.value.min,
|
|
137
|
-
max: m.value.max,
|
|
138
|
-
bucketCounts: [m.value.count],
|
|
139
|
-
explicitBounds: [],
|
|
140
|
-
});
|
|
141
|
-
// Map NrMetric.type → OTLP metric kind:
|
|
142
|
-
// - `gauge` → OTLP Gauge (point-in-time numeric value)
|
|
143
|
-
// - `count` → OTLP Sum (monotonic, DELTA aggregation temporality = 1).
|
|
144
|
-
// NrCountMetric carries intervalMs — it represents a bounded-interval
|
|
145
|
-
// delta, not a cumulative running total. Using CUMULATIVE (2) would
|
|
146
|
-
// cause downstream collectors to treat each harvest as a monotonically-
|
|
147
|
-
// increasing total, producing incorrect rate calculations (§TR1).
|
|
148
|
-
// - `summary` → OTLP Histogram (with explicit min/max/sum/count fields,
|
|
149
|
-
// no buckets) — see §4.9 note above on why histogram is the closest
|
|
150
|
-
// faithful mapping.
|
|
151
|
-
const otlpMetric = (m) => {
|
|
152
|
-
if (m.type === 'count') {
|
|
153
|
-
return {
|
|
154
|
-
name: m.name,
|
|
155
|
-
sum: {
|
|
156
|
-
dataPoints: [numericDataPoint(m)],
|
|
157
|
-
// 1 = DELTA — the value represents the count within intervalMs,
|
|
158
|
-
// not a cumulative total from a fixed epoch.
|
|
159
|
-
aggregationTemporality: 1,
|
|
160
|
-
isMonotonic: true,
|
|
161
|
-
},
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
if (m.type === 'summary') {
|
|
165
|
-
return {
|
|
166
|
-
name: m.name,
|
|
167
|
-
histogram: {
|
|
168
|
-
dataPoints: [summaryDataPoint(m)],
|
|
169
|
-
// Aggregation temporality 1 = DELTA (the count/sum/min/max
|
|
170
|
-
// describe the harvest interval, not a cumulative total).
|
|
171
|
-
aggregationTemporality: 1,
|
|
172
|
-
},
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
return { name: m.name, gauge: { dataPoints: [numericDataPoint(m)] } };
|
|
176
|
-
};
|
|
177
123
|
const payload = {
|
|
178
124
|
resourceMetrics: [
|
|
179
125
|
{
|
|
180
|
-
resource: {
|
|
181
|
-
attributes: Object.entries(this.resourceAttributes).map(([key, value]) => ({
|
|
182
|
-
key,
|
|
183
|
-
value: { stringValue: value },
|
|
184
|
-
})),
|
|
185
|
-
},
|
|
126
|
+
resource: { attributes: this.otlpResource },
|
|
186
127
|
scopeMetrics: [
|
|
187
128
|
{
|
|
188
|
-
scope:
|
|
189
|
-
metrics: metrics.map(otlpMetric),
|
|
129
|
+
scope: this.otlpScope,
|
|
130
|
+
metrics: metrics.map((m) => this.otlpMetric(m)),
|
|
190
131
|
},
|
|
191
132
|
],
|
|
192
133
|
},
|
|
@@ -203,34 +144,109 @@ export class OtlpTransport {
|
|
|
203
144
|
// level retry queue is rarely engaged for events. For metrics, we
|
|
204
145
|
// intentionally rely on the scheduler's retry queue instead — there
|
|
205
146
|
// is no PeriodicExportingMetricReader in this code path.
|
|
206
|
-
// Gzip-compress the payload to match sendWithRetry
|
|
147
|
+
// Gzip-compress the payload to match sendWithRetry. The NR OTLP
|
|
207
148
|
// endpoint accepts gzip; for large metric batches this is a 5-10× size win.
|
|
208
149
|
const compressed = await gzipAsync(JSON.stringify(payload));
|
|
209
150
|
const response = await fetch(`${this.endpoint}/v1/metrics`, {
|
|
210
151
|
method: 'POST',
|
|
211
|
-
headers:
|
|
212
|
-
'Content-Type': 'application/json',
|
|
213
|
-
'Content-Encoding': 'gzip',
|
|
214
|
-
'User-Agent': `${this.clientName}/${VERSION}`,
|
|
215
|
-
...this.headers,
|
|
216
|
-
},
|
|
152
|
+
headers: this.metricsHeaders,
|
|
217
153
|
body: compressed,
|
|
218
154
|
signal: AbortSignal.timeout(this.requestTimeoutMs),
|
|
219
|
-
//
|
|
155
|
+
// see http-client.ts for rationale.
|
|
220
156
|
keepalive: true,
|
|
221
157
|
});
|
|
222
158
|
if (!response.ok) {
|
|
223
159
|
const body = await response.text().catch(() => '');
|
|
224
160
|
const msg = `OTLP metric export failed: HTTP ${response.status}${body ? ` — ${body.slice(0, 256)}` : ''}`;
|
|
225
161
|
// 400 means the payload itself is malformed — retrying the same payload
|
|
226
|
-
// will always fail, so surface this as a distinct non-retryable error
|
|
162
|
+
// will always fail, so surface this as a distinct non-retryable error.
|
|
227
163
|
if (response.status === 400) {
|
|
228
164
|
throw Object.assign(new Error(msg), { code: 'OTLP_BAD_REQUEST' });
|
|
229
165
|
}
|
|
230
166
|
throw new Error(msg);
|
|
231
167
|
}
|
|
232
|
-
// Drain on success so undici returns the socket to the keep-alive pool
|
|
168
|
+
// Drain on success so undici returns the socket to the keep-alive pool.
|
|
233
169
|
await response.body?.cancel().catch(() => { });
|
|
234
170
|
}
|
|
171
|
+
otlpAttributes(attrs) {
|
|
172
|
+
return Object.entries(attrs ?? {}).map(([key, value]) => ({
|
|
173
|
+
key,
|
|
174
|
+
value: typeof value === 'number'
|
|
175
|
+
? { doubleValue: value }
|
|
176
|
+
: typeof value === 'boolean'
|
|
177
|
+
? { boolValue: value }
|
|
178
|
+
: { stringValue: String(value) },
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
numericDataPoint(m) {
|
|
182
|
+
return {
|
|
183
|
+
// For count (delta Sum) metrics, include startTimeUnixNano per the OTLP
|
|
184
|
+
// spec. Gauge data points do not require it but it is harmless.
|
|
185
|
+
// Clamp to 0: if timestamp < intervalMs (misconfigured metric), a negative
|
|
186
|
+
// startTimeUnixNano would be rejected by strict OTLP collectors.
|
|
187
|
+
startTimeUnixNano: m.type === 'count' ? Math.max(0, m.timestamp - m.intervalMs) * 1_000_000 : undefined,
|
|
188
|
+
timeUnixNano: m.timestamp * 1_000_000,
|
|
189
|
+
asDouble: m.value,
|
|
190
|
+
attributes: this.otlpAttributes(m.attributes),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
// summary is now a first-class type with a structured
|
|
194
|
+
// value `{ count, sum, min, max }`. OTLP doesn't have a single
|
|
195
|
+
// "Summary"-shaped metric kind; the closest faithful mapping is OTLP
|
|
196
|
+
// Histogram with explicit `count` and `sum` fields plus per-data-point
|
|
197
|
+
// `min` / `max`. Bucket boundaries are intentionally omitted: NR doesn't
|
|
198
|
+
// need them for summary aggregation, and emitting empty `bucketCounts`
|
|
199
|
+
// alongside `min`/`max` is the documented OTLP shape for unbucketed
|
|
200
|
+
// summaries (`explicitBounds: []`, `bucketCounts: [<count>]`).
|
|
201
|
+
summaryDataPoint(m) {
|
|
202
|
+
return {
|
|
203
|
+
// OTLP Histogram with DELTA temporality requires startTimeUnixNano.
|
|
204
|
+
startTimeUnixNano: Math.max(0, m.timestamp - m.intervalMs) * 1_000_000,
|
|
205
|
+
timeUnixNano: m.timestamp * 1_000_000,
|
|
206
|
+
attributes: this.otlpAttributes(m.attributes),
|
|
207
|
+
count: m.value.count,
|
|
208
|
+
sum: m.value.sum,
|
|
209
|
+
min: m.value.min,
|
|
210
|
+
max: m.value.max,
|
|
211
|
+
bucketCounts: [m.value.count],
|
|
212
|
+
explicitBounds: [],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
// Map NrMetric.type → OTLP metric kind:
|
|
216
|
+
// - `gauge` → OTLP Gauge (point-in-time numeric value)
|
|
217
|
+
// - `count` → OTLP Sum (monotonic, DELTA aggregation temporality = 1).
|
|
218
|
+
// NrCountMetric carries intervalMs — it represents a bounded-interval
|
|
219
|
+
// delta, not a cumulative running total. Using CUMULATIVE (2) would
|
|
220
|
+
// cause downstream collectors to treat each harvest as a monotonically-
|
|
221
|
+
// increasing total, producing incorrect rate calculations.
|
|
222
|
+
// - `summary` → OTLP Histogram (with explicit min/max/sum/count fields,
|
|
223
|
+
// no buckets) — see note above on why histogram is the closest
|
|
224
|
+
// faithful mapping.
|
|
225
|
+
otlpMetric(m) {
|
|
226
|
+
if (m.type === 'count') {
|
|
227
|
+
return {
|
|
228
|
+
name: m.name,
|
|
229
|
+
sum: {
|
|
230
|
+
dataPoints: [this.numericDataPoint(m)],
|
|
231
|
+
// 1 = DELTA — the value represents the count within intervalMs,
|
|
232
|
+
// not a cumulative total from a fixed epoch.
|
|
233
|
+
aggregationTemporality: 1,
|
|
234
|
+
isMonotonic: true,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (m.type === 'summary') {
|
|
239
|
+
return {
|
|
240
|
+
name: m.name,
|
|
241
|
+
histogram: {
|
|
242
|
+
dataPoints: [this.summaryDataPoint(m)],
|
|
243
|
+
// Aggregation temporality 1 = DELTA (the count/sum/min/max
|
|
244
|
+
// describe the harvest interval, not a cumulative total).
|
|
245
|
+
aggregationTemporality: 1,
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return { name: m.name, gauge: { dataPoints: [this.numericDataPoint(m)] } };
|
|
250
|
+
}
|
|
235
251
|
}
|
|
236
252
|
//# sourceMappingURL=otlp-transport.js.map
|