Add support for HTTP OpenTelemetry exporters (#6357)

This commit is contained in:
Billy Biggs
2025-08-15 18:10:21 -07:00
committed by GitHub
parent 4896c7739f
commit d57cc0b930
10 changed files with 301 additions and 34 deletions

View File

@@ -0,0 +1,104 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Config } from '../config/config.js';
import { initializeTelemetry, shutdownTelemetry } from './sdk.js';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
import { OTLPTraceExporter as OTLPTraceExporterHttp } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPLogExporter as OTLPLogExporterHttp } from '@opentelemetry/exporter-logs-otlp-http';
import { OTLPMetricExporter as OTLPMetricExporterHttp } from '@opentelemetry/exporter-metrics-otlp-http';
import { NodeSDK } from '@opentelemetry/sdk-node';
vi.mock('@opentelemetry/exporter-trace-otlp-grpc');
vi.mock('@opentelemetry/exporter-logs-otlp-grpc');
vi.mock('@opentelemetry/exporter-metrics-otlp-grpc');
vi.mock('@opentelemetry/exporter-trace-otlp-http');
vi.mock('@opentelemetry/exporter-logs-otlp-http');
vi.mock('@opentelemetry/exporter-metrics-otlp-http');
vi.mock('@opentelemetry/sdk-node');
describe('Telemetry SDK', () => {
let mockConfig: Config;
beforeEach(() => {
vi.clearAllMocks();
mockConfig = {
getTelemetryEnabled: () => true,
getTelemetryOtlpEndpoint: () => 'http://localhost:4317',
getTelemetryOtlpProtocol: () => 'grpc',
getTelemetryOutfile: () => undefined,
getDebugMode: () => false,
getSessionId: () => 'test-session',
} as unknown as Config;
});
afterEach(async () => {
await shutdownTelemetry(mockConfig);
});
it('should use gRPC exporters when protocol is grpc', () => {
initializeTelemetry(mockConfig);
expect(OTLPTraceExporter).toHaveBeenCalledWith({
url: 'http://localhost:4317',
compression: 'gzip',
});
expect(OTLPLogExporter).toHaveBeenCalledWith({
url: 'http://localhost:4317',
compression: 'gzip',
});
expect(OTLPMetricExporter).toHaveBeenCalledWith({
url: 'http://localhost:4317',
compression: 'gzip',
});
expect(NodeSDK.prototype.start).toHaveBeenCalled();
});
it('should use HTTP exporters when protocol is http', () => {
vi.spyOn(mockConfig, 'getTelemetryEnabled').mockReturnValue(true);
vi.spyOn(mockConfig, 'getTelemetryOtlpProtocol').mockReturnValue('http');
vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue(
'http://localhost:4318',
);
initializeTelemetry(mockConfig);
expect(OTLPTraceExporterHttp).toHaveBeenCalledWith({
url: 'http://localhost:4318/',
});
expect(OTLPLogExporterHttp).toHaveBeenCalledWith({
url: 'http://localhost:4318/',
});
expect(OTLPMetricExporterHttp).toHaveBeenCalledWith({
url: 'http://localhost:4318/',
});
expect(NodeSDK.prototype.start).toHaveBeenCalled();
});
it('should parse gRPC endpoint correctly', () => {
vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue(
'https://my-collector.com',
);
initializeTelemetry(mockConfig);
expect(OTLPTraceExporter).toHaveBeenCalledWith(
expect.objectContaining({ url: 'https://my-collector.com' }),
);
});
it('should parse HTTP endpoint correctly', () => {
vi.spyOn(mockConfig, 'getTelemetryOtlpProtocol').mockReturnValue('http');
vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue(
'https://my-collector.com',
);
initializeTelemetry(mockConfig);
expect(OTLPTraceExporterHttp).toHaveBeenCalledWith(
expect.objectContaining({ url: 'https://my-collector.com/' }),
);
});
});

View File

@@ -8,6 +8,9 @@ import { DiagConsoleLogger, DiagLogLevel, diag } from '@opentelemetry/api';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
import { OTLPTraceExporter as OTLPTraceExporterHttp } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPLogExporter as OTLPLogExporterHttp } from '@opentelemetry/exporter-logs-otlp-http';
import { OTLPMetricExporter as OTLPMetricExporterHttp } from '@opentelemetry/exporter-metrics-otlp-http';
import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
@@ -45,8 +48,9 @@ export function isTelemetrySdkInitialized(): boolean {
return telemetryInitialized;
}
function parseGrpcEndpoint(
function parseOtlpEndpoint(
otlpEndpointSetting: string | undefined,
protocol: 'grpc' | 'http',
): string | undefined {
if (!otlpEndpointSetting) {
return undefined;
@@ -56,9 +60,13 @@ function parseGrpcEndpoint(
try {
const url = new URL(trimmedEndpoint);
// OTLP gRPC exporters expect an endpoint in the format scheme://host:port
// The `origin` property provides this, stripping any path, query, or hash.
return url.origin;
if (protocol === 'grpc') {
// OTLP gRPC exporters expect an endpoint in the format scheme://host:port
// The `origin` property provides this, stripping any path, query, or hash.
return url.origin;
}
// For http, use the full href.
return url.href;
} catch (error) {
diag.error('Invalid OTLP endpoint URL provided:', trimmedEndpoint, error);
return undefined;
@@ -77,43 +85,70 @@ export function initializeTelemetry(config: Config): void {
});
const otlpEndpoint = config.getTelemetryOtlpEndpoint();
const grpcParsedEndpoint = parseGrpcEndpoint(otlpEndpoint);
const useOtlp = !!grpcParsedEndpoint;
const otlpProtocol = config.getTelemetryOtlpProtocol();
const parsedEndpoint = parseOtlpEndpoint(otlpEndpoint, otlpProtocol);
const useOtlp = !!parsedEndpoint;
const telemetryOutfile = config.getTelemetryOutfile();
const spanExporter = useOtlp
? new OTLPTraceExporter({
url: grpcParsedEndpoint,
let spanExporter:
| OTLPTraceExporter
| OTLPTraceExporterHttp
| FileSpanExporter
| ConsoleSpanExporter;
let logExporter:
| OTLPLogExporter
| OTLPLogExporterHttp
| FileLogExporter
| ConsoleLogRecordExporter;
let metricReader: PeriodicExportingMetricReader;
if (useOtlp) {
if (otlpProtocol === 'http') {
spanExporter = new OTLPTraceExporterHttp({
url: parsedEndpoint,
});
logExporter = new OTLPLogExporterHttp({
url: parsedEndpoint,
});
metricReader = new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporterHttp({
url: parsedEndpoint,
}),
exportIntervalMillis: 10000,
});
} else {
// grpc
spanExporter = new OTLPTraceExporter({
url: parsedEndpoint,
compression: CompressionAlgorithm.GZIP,
})
: telemetryOutfile
? new FileSpanExporter(telemetryOutfile)
: new ConsoleSpanExporter();
const logExporter = useOtlp
? new OTLPLogExporter({
url: grpcParsedEndpoint,
});
logExporter = new OTLPLogExporter({
url: parsedEndpoint,
compression: CompressionAlgorithm.GZIP,
})
: telemetryOutfile
? new FileLogExporter(telemetryOutfile)
: new ConsoleLogRecordExporter();
const metricReader = useOtlp
? new PeriodicExportingMetricReader({
});
metricReader = new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: grpcParsedEndpoint,
url: parsedEndpoint,
compression: CompressionAlgorithm.GZIP,
}),
exportIntervalMillis: 10000,
})
: telemetryOutfile
? new PeriodicExportingMetricReader({
exporter: new FileMetricExporter(telemetryOutfile),
exportIntervalMillis: 10000,
})
: new PeriodicExportingMetricReader({
exporter: new ConsoleMetricExporter(),
exportIntervalMillis: 10000,
});
});
}
} else if (telemetryOutfile) {
spanExporter = new FileSpanExporter(telemetryOutfile);
logExporter = new FileLogExporter(telemetryOutfile);
metricReader = new PeriodicExportingMetricReader({
exporter: new FileMetricExporter(telemetryOutfile),
exportIntervalMillis: 10000,
});
} else {
spanExporter = new ConsoleSpanExporter();
logExporter = new ConsoleLogRecordExporter();
metricReader = new PeriodicExportingMetricReader({
exporter: new ConsoleMetricExporter(),
exportIntervalMillis: 10000,
});
}
sdk = new NodeSDK({
resource,