Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
198 changes: 53 additions & 145 deletions experimental/packages/configuration/src/FileConfigFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ import * as yaml from 'yaml';
import type { ConfigFactory } from './IConfigFactory';
import { substituteEnvVars } from './utils';
import type {
BatchLogRecordProcessor,
BatchSpanProcessor,
CardinalityLimits,
AttributeNameValue,
ConfigurationModel,
PeriodicMetricReader,
TextMapPropagator,
} from './generated/types';
// Pre-compiled ajv validator — eliminates runtime schema compilation on cold start.
// Generated by `npm run generate:config`; do not import ajv directly here.
Expand Down Expand Up @@ -74,191 +72,101 @@ export function parseConfigFile(): ConfigurationModel {
// Strip file_format from output — it's a meta-field, not a config value
delete (data as Record<string, unknown>)['file_format'];

applyConfigDefaults(data);
mergeAttributesList(data);
mergeCompositeList(data);
applyBatchProcessorDefaults(data);
applyPeriodicReaderDefaults(data);
applyOtlpHttpEncodingDefaults(data);

return data;
}

/**
* Apply default encoding ('protobuf') to all otlp_http exporters in the config
* where encoding was not explicitly specified. The spec defines protobuf as the
* default encoding for OTLP HTTP exporters.
* Merge attributes from `resource.attributes` and `resource.attributes_list`
* (comma-separated key=value pairs) into an `AttributeNameValue[]` array.
* Entries already in `resource.attributes` taking precedence.
*
* Note: The returned array might not be a copy, so it should not be mutated.
*/
function applyOtlpHttpEncodingDefaults(data: ConfigurationModel): void {
const applyEncoding = (
exporter: { encoding?: string | null } | null | undefined
) => {
if (exporter && exporter.encoding == null) {
exporter.encoding = 'protobuf';
}
};

for (const processor of data.tracer_provider?.processors ?? []) {
applyEncoding(processor.batch?.exporter?.otlp_http);
applyEncoding(processor.simple?.exporter?.otlp_http);
}

for (const reader of data.meter_provider?.readers ?? []) {
applyEncoding(reader.periodic?.exporter?.otlp_http);
export function mergeResourceAttributesConfig(
attributes?: AttributeNameValue[],
attributes_list?: string | null
): AttributeNameValue[] | undefined {
if (!attributes_list) {
return attributes;
}

for (const processor of data.logger_provider?.processors ?? []) {
applyEncoding(processor.batch?.exporter?.otlp_http);
applyEncoding(processor.simple?.exporter?.otlp_http);
}
}
const mergedAttrs = attributes ? attributes.slice() : [];
const existingNames = new Set(mergedAttrs.map(a => a.name));

/**
* Merge resource.attributes_list (comma-separated key=value pairs) into
* resource.attributes, with entries already in attributes taking precedence.
*
* Per the spec, `,` and `=` in keys and values MUST be percent-encoded, and
* other characters MAY be percent-encoded. On any parse or decode error, the
* entire attributes_list is discarded and a warning is emitted.
* See https://opentelemetry.io/docs/specs/otel/resource/sdk/#specifying-resource-information-via-an-environment-variable
*/
function mergeAttributesList(data: ConfigurationModel): void {
const resource = data.resource;
const list = resource?.attributes_list;
if (typeof list !== 'string' || !list.trim()) return;

const decoded: Array<{ key: string; value: string }> = [];
for (const pair of list.split(',')) {
// TODO: handle attribute limits, if any
Comment thread
trentm marked this conversation as resolved.
Outdated
let discard = false;
const listAttrs: Record<string, string> = {};
for (const pair of attributes_list.split(',')) {
if (pair.trim() === '') continue;

// Per spec, `=` must be percent-encoded in keys/values, so a valid entry
// splits into exactly two parts.
const parts = pair.split('=');
if (parts.length !== 2) {
diag.warn(
`Invalid format for resource.attributes_list entry "${pair}": expected key=value with '=' percent-encoded in keys/values. Discarding all entries.`
);
return;
discard = true;
break;
}

const rawKey = parts[0].trim();
const rawName = parts[0].trim();
const rawValue = parts[1].trim();
if (rawKey === '') {
if (rawName === '') {
diag.warn(
`Empty attribute key in resource.attributes_list entry "${pair}". Discarding all entries.`
`Empty attribute name in resource.attributes_list entry "${pair}". Discarding all entries.`
);
return;
discard = true;
break;
}

try {
decoded.push({
key: decodeURIComponent(rawKey),
value: decodeURIComponent(rawValue),
});
listAttrs[decodeURIComponent(rawName)] = decodeURIComponent(rawValue);
} catch (e) {
diag.warn(
`Failed to percent-decode resource.attributes_list entry "${pair}", discarding all entries: ${e}`
);
return;
discard = true;
break;
}
}

if (resource!.attributes == null) {
resource!.attributes = [];
}

const existingKeys = new Set(
resource!.attributes.map((a: { name: string }) => a.name)
);

for (const { key, value } of decoded) {
if (!existingKeys.has(key)) {
resource!.attributes.push({ name: key, value, type: 'string' });
if (!discard) {
for (const [name, value] of Object.entries(listAttrs)) {
if (!existingNames.has(name)) {
mergedAttrs.push({ name, value, type: 'string' });
}
}
}

return mergedAttrs;
}

/**
* Merge propagator.composite_list (comma-separated propagator names) into
* propagator.composite, with entries already in composite taking precedence.
* Merge TextMapPropagator configs from `propagator.composite` and `propagator.composite_list`
* (comma-separated names) into a `TextMapPropagator[]` array.
* Entries already in `propagator.composite` taking precedence.
*
* Note: The returned array might not be a copy, so it should not be mutated.
*/
function mergeCompositeList(data: ConfigurationModel): void {
const propagator = data.propagator;
const list = propagator?.composite_list;
if (typeof list !== 'string' || !list.trim()) return;

if (propagator!.composite == null) {
propagator!.composite = [];
export function mergePropagatorCompositeConfig(
composite?: TextMapPropagator[],
composite_list?: string | null
): TextMapPropagator[] | undefined {
if (!composite_list) {
return composite;
}

const existingNames = new Set(
(propagator!.composite as Array<Record<string, unknown>>).map(
entry => Object.keys(entry)[0]
)
);
const mergedComposite = composite ? composite.slice() : [];
const existingNames = new Set(mergedComposite.map(c => Object.keys(c)[0]));

for (const name of list.split(',')) {
for (const name of composite_list.split(',')) {
const trimmed = name.trim();
if (trimmed && !existingNames.has(trimmed)) {
(propagator!.composite as Array<Record<string, unknown>>).push({
[trimmed]: {},
});
mergedComposite.push({ [trimmed]: null });
existingNames.add(trimmed);
}
}
}

/**
* Apply spec-defined defaults for batch span/log-record processor fields that
* are not encoded in the JSON schema.
*/
function applyBatchProcessorDefaults(data: ConfigurationModel): void {
const applyDefaults = (
batch: BatchSpanProcessor | BatchLogRecordProcessor
) => {
if (batch.schedule_delay == null) batch.schedule_delay = 5000;
if (batch.export_timeout == null) batch.export_timeout = 30000;
if (batch.max_queue_size == null) batch.max_queue_size = 2048;
if (batch.max_export_batch_size == null) batch.max_export_batch_size = 512;
};

for (const processor of data.tracer_provider?.processors ?? []) {
if (processor.batch) applyDefaults(processor.batch);
}
for (const processor of data.logger_provider?.processors ?? []) {
if (processor.batch) applyDefaults(processor.batch);
}
}

/**
* Apply spec-defined defaults for periodic metric reader fields.
*/
function applyPeriodicReaderDefaults(data: ConfigurationModel): void {
for (const reader of data.meter_provider?.readers ?? []) {
const periodic = reader.periodic as PeriodicMetricReader | undefined;
if (!periodic) continue;
if (periodic.interval == null) periodic.interval = 60000;
if (periodic.timeout == null) periodic.timeout = 30000;
if (periodic.cardinality_limits == null) {
periodic.cardinality_limits = { default: 2000 } as CardinalityLimits;
}
}
}

/**
* Apply spec-defined defaults that are not encoded in the JSON schema.
* Both FileConfigFactory and EnvironmentConfigFactory apply the same defaults
* so consumers see consistent behaviour regardless of config source.
*/
function applyConfigDefaults(data: ConfigurationModel): void {
if (data.disabled == null) {
data.disabled = false;
}
if (data.log_level == null) {
data.log_level = 'info';
}
if (data.attribute_limits == null) {
data.attribute_limits = { attribute_count_limit: 128 };
} else if (data.attribute_limits.attribute_count_limit == null) {
data.attribute_limits.attribute_count_limit = 128;
}
return mergedComposite;
}
5 changes: 5 additions & 0 deletions experimental/packages/configuration/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,10 @@ export type {
HttpTls as HttpTlsConfigModel,
GrpcTls as GrpcTlsConfigModel,
SeverityNumber as SeverityNumberConfigModel,
TextMapPropagator as TextMapPropagatorConfigModel,
} from './generated/types';
export { createConfigFactory } from './ConfigFactory';
export {
mergeResourceAttributesConfig,
mergePropagatorCompositeConfig,
} from './FileConfigFactory';
Loading
Loading