Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions experimental/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2

### :bug: Bug Fixes

* fix(sdk-node): pass OTLP HTTP metric exporter configuration from declarative config [#6665](https://github.com/open-telemetry/opentelemetry-js/issues/6665) @cyphercodes
* fix(configuration): do not validate `OTEL_CONFIG_FILE` value before using it for file config [#6643](https://github.com/open-telemetry/opentelemetry-js/pull/6643) @trentm
* fix(configuration): improve how 'additionalProperties' in JSON schema is translated to TS types [#6650](https://github.com/open-telemetry/opentelemetry-js/pull/6650) @trentm
* fix(configuration): remove stripMinItems and preprocessNullArrays from validation/parsing [#6657](https://github.com/open-telemetry/opentelemetry-js/pull/6657) @trentm
Expand Down
101 changes: 79 additions & 22 deletions experimental/packages/opentelemetry-sdk-node/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getNumberFromEnv,
getStringFromEnv,
getStringListFromEnv,
parseKeyPairsIntoRecord,
W3CBaggagePropagator,
W3CTraceContextPropagator,
} from '@opentelemetry/core';
Expand Down Expand Up @@ -65,6 +66,7 @@ import type {
} from '@opentelemetry/configuration';
import type {
AggregationOption,
AggregationSelector,
IAttributesProcessor,
IMetricReader,
PushMetricExporter,
Expand All @@ -79,7 +81,10 @@ import {
PeriodicExportingMetricReader,
} from '@opentelemetry/sdk-metrics';
import { OTLPMetricExporter as OTLPGrpcMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
import { OTLPMetricExporter as OTLPHttpMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import {
AggregationTemporalityPreference,
OTLPMetricExporter as OTLPHttpMetricExporter,
} from '@opentelemetry/exporter-metrics-otlp-http';
import { OTLPMetricExporter as OTLPProtoMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto';
import type {
BufferConfig,
Expand Down Expand Up @@ -509,37 +514,88 @@ export function getOtlpMetricExporterFromEnv(): PushMetricExporter {
return new OTLPProtoMetricExporter();
}

type OtlpHttpMetricExporterConfigModel = NonNullable<
PeriodicMetricReaderConfigModel['exporter']['otlp_http']
>;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: This type could be exported from the configuration package and used directly.


function getMetricExporterCompression(
compression: string | undefined
): CompressionAlgorithm {
return compression === 'gzip'
? CompressionAlgorithm.GZIP
: CompressionAlgorithm.NONE;
}

function getMetricExporterTemporalityPreference(
temporalityPreference: OtlpHttpMetricExporterConfigModel['temporality_preference']
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: Likewise, we could add an ExporterTemporalityPreferenceConfigModel export from the configuration package.

): AggregationTemporalityPreference | undefined {
switch (temporalityPreference) {
case 'delta':
return AggregationTemporalityPreference.DELTA;
case 'low_memory':
return AggregationTemporalityPreference.LOWMEMORY;
case 'cumulative':
return AggregationTemporalityPreference.CUMULATIVE;
default:
return undefined;
}
}

function getMetricExporterDefaultHistogramAggregation(
defaultHistogramAggregation: OtlpHttpMetricExporterConfigModel['default_histogram_aggregation']
): AggregationSelector | undefined {
if (defaultHistogramAggregation !== 'base2_exponential_bucket_histogram') {
return undefined;
}

return instrumentType => {
if (instrumentType === InstrumentType.HISTOGRAM) {
return { type: AggregationType.EXPONENTIAL_HISTOGRAM };
}
return { type: AggregationType.DEFAULT };
};
}

function getOtlpHttpMetricExporterConfig(
config: OtlpHttpMetricExporterConfigModel
) {
return {
compression: getMetricExporterCompression(config.compression),
url: config.endpoint,
headers: getHeadersFromConfiguration(config.headers, config.headers_list),
timeoutMillis: config.timeout,
httpAgentOptions: getHttpAgentOptionsFromTls(config.tls),
temporalityPreference: getMetricExporterTemporalityPreference(
config.temporality_preference
),
aggregationPreference: getMetricExporterDefaultHistogramAggregation(
config.default_histogram_aggregation
),
};
}

export function getPeriodicMetricReaderFromConfiguration(
periodic: PeriodicMetricReaderConfigModel
): IMetricReader | undefined {
if (periodic.exporter) {
let exporter;
if (periodic.exporter.otlp_http) {
const encoding = periodic.exporter.otlp_http.encoding;
const config = periodic.exporter.otlp_http;
const encoding = config.encoding ?? 'protobuf';
const exporterConfig = getOtlpHttpMetricExporterConfig(config);
if (encoding === 'json') {
exporter = new OTLPHttpMetricExporter({
compression:
periodic.exporter.otlp_http.compression === 'gzip'
? CompressionAlgorithm.GZIP
: CompressionAlgorithm.NONE,
});
exporter = new OTLPHttpMetricExporter(exporterConfig);
} else if (encoding === 'protobuf') {
exporter = new OTLPProtoMetricExporter({
compression:
periodic.exporter.otlp_http.compression === 'gzip'
? CompressionAlgorithm.GZIP
: CompressionAlgorithm.NONE,
});
exporter = new OTLPProtoMetricExporter(exporterConfig);
} else {
diag.warn(`Unsupported OTLP metrics encoding: ${encoding}.`);
}
}
if (periodic.exporter.otlp_grpc) {
exporter = new OTLPGrpcMetricExporter({
compression:
periodic.exporter.otlp_grpc.compression === 'gzip'
? CompressionAlgorithm.GZIP
: CompressionAlgorithm.NONE,
compression: getMetricExporterCompression(
periodic.exporter.otlp_grpc.compression
),
});
}

Expand Down Expand Up @@ -681,13 +737,14 @@ export function getLogRecordProcessorsFromConfiguration(
}

export function getHeadersFromConfiguration(
headers: NameStringValuePairConfigModel[] | undefined
headers: NameStringValuePairConfigModel[] | undefined,
headersList?: string
): Record<string, string> | undefined {
if (!headers) {
if (!headers && headersList === undefined) {
return undefined;
}
const result: Record<string, string> = {};
headers.forEach(header => {
const result: Record<string, string> = parseKeyPairsIntoRecord(headersList);
headers?.forEach(header => {
result[header.name] = header.value;
});
return result;
Expand Down
123 changes: 122 additions & 1 deletion experimental/packages/opentelemetry-sdk-node/test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,58 @@ import {
serviceInstanceIdDetector,
} from '@opentelemetry/resources';
import type { LoggerProviderConfig } from '@opentelemetry/sdk-logs';
import { AggregationType, InstrumentType } from '@opentelemetry/sdk-metrics';
import type { AggregationOption } from '@opentelemetry/sdk-metrics';
import {
AggregationTemporality,
AggregationType,
InstrumentType,
} from '@opentelemetry/sdk-metrics';
import type { SpanLimits } from '@opentelemetry/sdk-trace-node';

interface OtlpHttpTransportParameters {
url: string;
compression: string;
headers: () => Promise<Record<string, string>>;
agentFactory: (protocol: string) => Promise<{
options: {
ca?: Buffer;
cert?: Buffer;
key?: Buffer;
};
}>;
}

interface OtlpMetricExporterInternals {
_delegate: {
_timeout: number;
_transport: {
_transport: {
_parameters: OtlpHttpTransportParameters;
};
};
};
selectAggregation(instrumentType: InstrumentType): AggregationOption;
selectAggregationTemporality(
instrumentType: InstrumentType
): AggregationTemporality;
}

interface PeriodicMetricReaderInternals {
_exporter: OtlpMetricExporterInternals;
}

function getMetricExporterInternals(
reader: unknown
): OtlpMetricExporterInternals {
return (reader as PeriodicMetricReaderInternals)._exporter;
}

function getHttpTransportParameters(
exporter: OtlpMetricExporterInternals
): OtlpHttpTransportParameters {
return exporter._delegate._transport._transport._parameters;
}

describe('getPropagatorFromEnv', function () {
afterEach(() => {
delete process.env.OTEL_PROPAGATORS;
Expand Down Expand Up @@ -433,6 +482,78 @@ describe('getBatchLogRecordProcessorConfigFromEnv', function () {
);
});

it('passes OTLP HTTP metric exporter connection options from configuration', async function () {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: Move this it(...) case to a new describe('getPeriodicMetricReaderFromConfiguration', ... section. If I read this correctly, this is currently in the getBatchLogRecordProcessorConfigFromEnv describe block.

const reader = getPeriodicMetricReaderFromConfiguration({
interval: 7000,
timeout: 5000,
exporter: {
otlp_http: {
endpoint: 'https://collector.example/v1/metrics',
tls: {
ca_file: 'test/fixtures/ca.pem',
key_file: 'test/fixtures/ca-key.pem',
cert_file: 'test/fixtures/cert.pem',
},
headers_list: 'x-list=list-value,x-overridden=list-value',
headers: [
{ name: 'x-test-header', value: 'test-value' },
{ name: 'x-overridden', value: 'header-value' },
],
compression: 'gzip',
timeout: 1234,
},
},
});

assert.ok(reader);
const exporter = getMetricExporterInternals(reader);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could the same thing be accomplished with a type assertion to any? E.g.:

Suggested change
const exporter = getMetricExporterInternals(reader);
const exporter = (reader as any)._exporter;

I guess I'm expressing a personal preference to avoid defining interface FooInterals types that give the veneer of type safety for spelunking into internal object details.

const parameters = getHttpTransportParameters(exporter);

assert.strictEqual(parameters.url, 'https://collector.example/v1/metrics');
assert.strictEqual(parameters.compression, 'gzip');
assert.strictEqual(exporter._delegate._timeout, 1234);
assert.deepStrictEqual(await parameters.headers(), {
'x-list': 'list-value',
'x-overridden': 'header-value',
'x-test-header': 'test-value',
'Content-Type': 'application/x-protobuf',
});

const agent = await parameters.agentFactory('https:');
assert.ok(agent.options.ca);
assert.ok(agent.options.key);
assert.ok(agent.options.cert);
});

it('passes OTLP HTTP metric exporter aggregation options from configuration', function () {
const reader = getPeriodicMetricReaderFromConfiguration({
exporter: {
otlp_http: {
encoding: 'json',
temporality_preference: 'delta',
default_histogram_aggregation: 'base2_exponential_bucket_histogram',
},
},
});

assert.ok(reader);
const exporter = getMetricExporterInternals(reader);

assert.strictEqual(
exporter.selectAggregationTemporality(InstrumentType.COUNTER),
AggregationTemporality.DELTA
);
assert.deepStrictEqual(
exporter.selectAggregation(InstrumentType.HISTOGRAM),
{
type: AggregationType.EXPONENTIAL_HISTOGRAM,
}
);
assert.deepStrictEqual(exporter.selectAggregation(InstrumentType.COUNTER), {
type: AggregationType.DEFAULT,
});
});

it('should return values for getInstrumentType', function () {
assert.deepStrictEqual(
getInstrumentType('counter' as InstrumentTypeConfigModel),
Expand Down
Loading