Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions experimental/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ feat(configuration): parse config for rc 3 [#6304](https://github.com/open-telem
### :bug: Bug Fixes

* fix(exporter-prometheus): add missing `@opentelemetry/semantic-conventions` dependency [#6330](https://github.com/open-telemetry/opentelemetry-js/pull/6330) @omizha
* fix(otlp-transformer): correctly handle Uint8Array attribute values when serializing to JSON [#6348](https://github.com/open-telemetry/opentelemetry-js/pull/6348) @pichlermarc

### :books: Documentation

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export interface IAnyValue {
kvlistValue?: IKeyValueList;

/** AnyValue bytesValue */
bytesValue?: Uint8Array;
bytesValue?: Uint8Array | string;
}

/** Properties of an ArrayValue. */
Expand Down
34 changes: 24 additions & 10 deletions experimental/packages/otlp-transformer/src/common/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@ import type {
import { Attributes } from '@opentelemetry/api';
import { InstrumentationScope } from '@opentelemetry/core';
import { Resource as ISdkResource } from '@opentelemetry/resources';
import type { Encoder } from './utils';

export function createResource(resource: ISdkResource): Resource {
export function createResource(
resource: ISdkResource,
encoder: Encoder
): Resource {
const result: Resource = {
attributes: toAttributes(resource.attributes),
attributes: toAttributes(resource.attributes, encoder),
droppedAttributesCount: 0,
};

Expand All @@ -44,30 +48,40 @@ export function createInstrumentationScope(
};
}

export function toAttributes(attributes: Attributes): IKeyValue[] {
return Object.keys(attributes).map(key => toKeyValue(key, attributes[key]));
export function toAttributes(
attributes: Attributes,
encoder: Encoder
): IKeyValue[] {
return Object.keys(attributes).map(key =>
toKeyValue(key, attributes[key], encoder)
);
}

export function toKeyValue(key: string, value: unknown): IKeyValue {
export function toKeyValue(
key: string,
value: unknown,
encoder: Encoder
): IKeyValue {
return {
key: key,
value: toAnyValue(value),
value: toAnyValue(value, encoder),
};
}

export function toAnyValue(value: unknown): IAnyValue {
export function toAnyValue(value: unknown, encoder: Encoder): IAnyValue {
const t = typeof value;
if (t === 'string') return { stringValue: value as string };
if (t === 'number') {
if (!Number.isInteger(value)) return { doubleValue: value as number };
return { intValue: value as number };
}
if (t === 'boolean') return { boolValue: value as boolean };
if (value instanceof Uint8Array) return { bytesValue: value };
if (value instanceof Uint8Array)
return { bytesValue: encoder.encodeUint8Array(value) };
if (Array.isArray(value)) {
const values: IAnyValue[] = new Array(value.length);
for (let i = 0; i < value.length; i++) {
values[i] = toAnyValue(value[i]);
values[i] = toAnyValue(value[i], encoder);
}
return { arrayValue: { values } };
}
Expand All @@ -77,7 +91,7 @@ export function toAnyValue(value: unknown): IAnyValue {
for (let i = 0; i < keys.length; i++) {
values[i] = {
key: keys[i],
value: toAnyValue((value as Record<string, unknown>)[keys[i]]),
value: toAnyValue((value as Record<string, unknown>)[keys[i]], encoder),
};
}
return { kvlistValue: { values } };
Expand Down
46 changes: 32 additions & 14 deletions experimental/packages/otlp-transformer/src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import type { OtlpEncodingOptions, Fixed64, LongBits } from './internal-types';
import type { Fixed64, LongBits } from './internal-types';
import { HrTime } from '@opentelemetry/api';
import { hrTimeToNanoseconds } from '@opentelemetry/core';
import { hexToBinary } from './hex-to-binary';
Expand Down Expand Up @@ -52,11 +52,15 @@ export type SpanContextEncodeFunction = (
export type OptionalSpanContextEncodeFunction = (
spanContext: string | undefined
) => string | Uint8Array | undefined;
export type Uint8ArrayEncodeFunction = (
value: Uint8Array
) => string | Uint8Array;

export interface Encoder {
encodeHrTime: HrTimeEncodeFunction;
encodeSpanContext: SpanContextEncodeFunction;
encodeOptionalSpanContext: OptionalSpanContextEncodeFunction;
encodeUint8Array: Uint8ArrayEncodeFunction;
}

function identity<T>(value: T): T {
Expand All @@ -68,22 +72,36 @@ function optionalHexToBinary(str: string | undefined): Uint8Array | undefined {
return hexToBinary(str);
}

const DEFAULT_ENCODER: Encoder = {
/**
* Encoder for protobuf format.
* Uses { high, low } timestamps and binary for span/trace IDs, leaves Uint8Array attributes as-is.
*/
export const PROTOBUF_ENCODER: Encoder = {
encodeHrTime: encodeAsLongBits,
encodeSpanContext: hexToBinary,
encodeOptionalSpanContext: optionalHexToBinary,
encodeUint8Array: identity,
};

export function getOtlpEncoder(options?: OtlpEncodingOptions): Encoder {
if (options === undefined) {
return DEFAULT_ENCODER;
}
/**
* Encoder for JSON format.
* Uses string timestamps, hex for span/trace IDs, and base64 for Uint8Array.
*/
export const JSON_ENCODER: Encoder = {
encodeHrTime: encodeTimestamp,
encodeSpanContext: identity,
encodeOptionalSpanContext: identity,
encodeUint8Array: (bytes: Uint8Array): string => {
if (typeof Buffer !== 'undefined') {
return Buffer.from(bytes).toString('base64');
}

const useLongBits = options.useLongBits ?? true;
const useHex = options.useHex ?? false;
return {
encodeHrTime: useLongBits ? encodeAsLongBits : encodeTimestamp,
encodeSpanContext: useHex ? identity : hexToBinary,
encodeOptionalSpanContext: useHex ? identity : optionalHexToBinary,
};
}
// implementation note: not using spread operator and passing to
// btoa to avoid stack overflow on large Uint8Arrays
const chars = new Array(bytes.length);
for (let i = 0; i < bytes.length; i++) {
chars[i] = String.fromCharCode(bytes[i]);
}
return btoa(chars.join(''));
},
};
22 changes: 13 additions & 9 deletions experimental/packages/otlp-transformer/src/logs/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,21 @@ import {
IResourceLogs,
} from './internal-types';
import { Resource } from '@opentelemetry/resources';
import { Encoder, getOtlpEncoder } from '../common/utils';
import { Encoder } from '../common/utils';
import {
createInstrumentationScope,
createResource,
toAnyValue,
toKeyValue,
} from '../common/internal';
import { SeverityNumber } from '@opentelemetry/api-logs';
import { OtlpEncodingOptions, IKeyValue } from '../common/internal-types';
import { IKeyValue } from '../common/internal-types';
import { LogAttributes } from '@opentelemetry/api-logs';

export function createExportLogsServiceRequest(
logRecords: ReadableLogRecord[],
options?: OtlpEncodingOptions
encoder: Encoder
): IExportLogsServiceRequest {
const encoder = getOtlpEncoder(options);
return {
resourceLogs: logRecordsToResourceLogs(logRecords, encoder),
};
Expand Down Expand Up @@ -80,7 +79,7 @@ function logRecordsToResourceLogs(
): IResourceLogs[] {
const resourceMap = createResourceMap(logRecords);
return Array.from(resourceMap, ([resource, ismMap]) => {
const processedResource = createResource(resource);
const processedResource = createResource(resource, encoder);
return {
resource: processedResource,
scopeLogs: Array.from(ismMap, ([, scopeLogs]) => {
Expand All @@ -101,9 +100,9 @@ function toLogRecord(log: ReadableLogRecord, encoder: Encoder): ILogRecord {
observedTimeUnixNano: encoder.encodeHrTime(log.hrTimeObserved),
severityNumber: toSeverityNumber(log.severityNumber),
severityText: log.severityText,
body: toAnyValue(log.body),
body: toAnyValue(log.body, encoder),
eventName: log.eventName,
attributes: toLogAttributes(log.attributes),
attributes: toLogAttributes(log.attributes, encoder),
droppedAttributesCount: log.droppedAttributesCount,
flags: log.spanContext?.traceFlags,
traceId: encoder.encodeOptionalSpanContext(log.spanContext?.traceId),
Expand All @@ -117,6 +116,11 @@ function toSeverityNumber(
return severityNumber as number | undefined as ESeverityNumber | undefined;
}

export function toLogAttributes(attributes: LogAttributes): IKeyValue[] {
return Object.keys(attributes).map(key => toKeyValue(key, attributes[key]));
export function toLogAttributes(
attributes: LogAttributes,
encoder: Encoder
): IKeyValue[] {
return Object.keys(attributes).map(key =>
toKeyValue(key, attributes[key], encoder)
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ISerializer } from '../../i-serializer';
import { ReadableLogRecord } from '@opentelemetry/sdk-logs';
import { createExportLogsServiceRequest } from '../internal';
import { IExportLogsServiceResponse } from '../export-response';
import { JSON_ENCODER } from '../../common/utils';

/*
* @experimental this serializer may receive breaking changes in minor versions, pin this package's version when using this constant
Expand All @@ -26,10 +27,7 @@ export const JsonLogsSerializer: ISerializer<
IExportLogsServiceResponse
> = {
serializeRequest: (arg: ReadableLogRecord[]) => {
const request = createExportLogsServiceRequest(arg, {
useHex: true,
useLongBits: false,
});
const request = createExportLogsServiceRequest(arg, JSON_ENCODER);
const encoder = new TextEncoder();
return encoder.encode(JSON.stringify(request));
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { createExportLogsServiceRequest } from '../internal';
import { ReadableLogRecord } from '@opentelemetry/sdk-logs';
import { ExportType } from '../../common/protobuf/protobuf-export-type';
import { ISerializer } from '../../i-serializer';
import { PROTOBUF_ENCODER } from '../../common/utils';

const logsResponseType = root.opentelemetry.proto.collector.logs.v1
.ExportLogsServiceResponse as ExportType<IExportLogsServiceResponse>;
Expand All @@ -37,7 +38,7 @@ export const ProtobufLogsSerializer: ISerializer<
IExportLogsServiceResponse
> = {
serializeRequest: (arg: ReadableLogRecord[]) => {
const request = createExportLogsServiceRequest(arg);
const request = createExportLogsServiceRequest(arg, PROTOBUF_ENCODER);
return logsRequestType.encode(request).finish();
},
deserializeResponse: (arg: Uint8Array) => {
Expand Down
18 changes: 8 additions & 10 deletions experimental/packages/otlp-transformer/src/metrics/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { OtlpEncodingOptions } from '../common/internal-types';
import { ValueType } from '@opentelemetry/api';
import {
AggregationTemporality,
Expand All @@ -35,7 +34,7 @@ import {
IResourceMetrics,
IScopeMetrics,
} from './internal-types';
import { Encoder, getOtlpEncoder } from '../common/utils';
import { Encoder } from '../common/utils';
import {
createInstrumentationScope,
createResource,
Expand All @@ -44,10 +43,9 @@ import {

export function toResourceMetrics(
resourceMetrics: ResourceMetrics,
options?: OtlpEncodingOptions
encoder: Encoder
): IResourceMetrics {
const encoder = getOtlpEncoder(options);
const processedResource = createResource(resourceMetrics.resource);
const processedResource = createResource(resourceMetrics.resource, encoder);
return {
resource: processedResource,
schemaUrl: processedResource.schemaUrl,
Expand Down Expand Up @@ -118,7 +116,7 @@ function toSingularDataPoint(
encoder: Encoder
) {
const out: INumberDataPoint = {
attributes: toAttributes(dataPoint.attributes),
attributes: toAttributes(dataPoint.attributes, encoder),
startTimeUnixNano: encoder.encodeHrTime(dataPoint.startTime),
timeUnixNano: encoder.encodeHrTime(dataPoint.endTime),
};
Expand Down Expand Up @@ -155,7 +153,7 @@ function toHistogramDataPoints(
return metricData.dataPoints.map(dataPoint => {
const histogram = dataPoint.value as Histogram;
return {
attributes: toAttributes(dataPoint.attributes),
attributes: toAttributes(dataPoint.attributes, encoder),
bucketCounts: histogram.buckets.counts,
explicitBounds: histogram.buckets.boundaries,
count: histogram.count,
Expand All @@ -175,7 +173,7 @@ function toExponentialHistogramDataPoints(
return metricData.dataPoints.map(dataPoint => {
const histogram = dataPoint.value as ExponentialHistogram;
return {
attributes: toAttributes(dataPoint.attributes),
attributes: toAttributes(dataPoint.attributes, encoder),
count: histogram.count,
min: histogram.min,
max: histogram.max,
Expand Down Expand Up @@ -209,11 +207,11 @@ function toAggregationTemporality(

export function createExportMetricsServiceRequest(
resourceMetrics: ResourceMetrics[],
options?: OtlpEncodingOptions
encoder: Encoder
): IExportMetricsServiceRequest {
return {
resourceMetrics: resourceMetrics.map(metrics =>
toResourceMetrics(metrics, options)
toResourceMetrics(metrics, encoder)
),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@ import { ISerializer } from '../../i-serializer';
import { ResourceMetrics } from '@opentelemetry/sdk-metrics';
import { createExportMetricsServiceRequest } from '../internal';
import { IExportMetricsServiceResponse } from '../export-response';
import { JSON_ENCODER } from '../../common/utils';

export const JsonMetricsSerializer: ISerializer<
ResourceMetrics,
IExportMetricsServiceResponse
> = {
serializeRequest: (arg: ResourceMetrics) => {
const request = createExportMetricsServiceRequest([arg], {
useLongBits: false,
});
const request = createExportMetricsServiceRequest([arg], JSON_ENCODER);
const encoder = new TextEncoder();
return encoder.encode(JSON.stringify(request));
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ExportType } from '../../common/protobuf/protobuf-export-type';
import { createExportMetricsServiceRequest } from '../internal';
import { ResourceMetrics } from '@opentelemetry/sdk-metrics';
import { IExportMetricsServiceResponse } from '../export-response';
import { PROTOBUF_ENCODER } from '../../common/utils';

const metricsResponseType = root.opentelemetry.proto.collector.metrics.v1
.ExportMetricsServiceResponse as ExportType<IExportMetricsServiceResponse>;
Expand All @@ -33,7 +34,7 @@ export const ProtobufMetricsSerializer: ISerializer<
IExportMetricsServiceResponse
> = {
serializeRequest: (arg: ResourceMetrics) => {
const request = createExportMetricsServiceRequest([arg]);
const request = createExportMetricsServiceRequest([arg], PROTOBUF_ENCODER);
return metricsRequestType.encode(request).finish();
},
deserializeResponse: (arg: Uint8Array) => {
Expand Down
Loading