diff --git a/sdk/monitor/monitor-opentelemetry-exporter/src/utils/common.ts b/sdk/monitor/monitor-opentelemetry-exporter/src/utils/common.ts index eaa0437ae0f3..f12fb48e2bd5 100644 --- a/sdk/monitor/monitor-opentelemetry-exporter/src/utils/common.ts +++ b/sdk/monitor/monitor-opentelemetry-exporter/src/utils/common.ts @@ -44,6 +44,7 @@ import { KnownContextTagKeys, TelemetryItem as Envelope, MetricsData } from "../ import { Resource } from "@opentelemetry/resources"; import { Attributes, HrTime } from "@opentelemetry/api"; import { hrTimeToNanoseconds } from "@opentelemetry/core"; +import { AnyValue } from "@opentelemetry/api-logs"; export function hrTimeToDate(hrTime: HrTime): Date { return new Date(hrTimeToNanoseconds(hrTime) / 1000000); @@ -255,6 +256,24 @@ export function createResourceMetricEnvelope( return; } +export function serializeAttribute(value: AnyValue): string { + if (typeof value === "object") { + if (value instanceof Uint8Array) { + return String(value); + } else { + try { + // Should handle Error objects as well + return JSON.stringify(value, Object.getOwnPropertyNames(value)); + } catch (err: unknown) { + // Failed to serialize, return string cast + return String(value); + } + } + } + // Return scalar and undefined values + return String(value); +} + export function shouldCreateResourceMetric(): boolean { return !(process.env.ENV_OPENTELEMETRY_RESOURCE_METRIC_DISABLED?.toLowerCase() === "true"); } diff --git a/sdk/monitor/monitor-opentelemetry-exporter/src/utils/logUtils.ts b/sdk/monitor/monitor-opentelemetry-exporter/src/utils/logUtils.ts index f224a012d006..2ba563517c9b 100644 --- a/sdk/monitor/monitor-opentelemetry-exporter/src/utils/logUtils.ts +++ b/sdk/monitor/monitor-opentelemetry-exporter/src/utils/logUtils.ts @@ -13,7 +13,7 @@ import { TelemetryExceptionData, TelemetryExceptionDetails, } from "../generated"; -import { createTagsFromResource, hrTimeToDate } from "./common"; +import { createTagsFromResource, hrTimeToDate, serializeAttribute } from "./common"; import { ReadableLogRecord } from "@opentelemetry/sdk-logs"; import { SEMATTRS_EXCEPTION_MESSAGE, @@ -135,7 +135,7 @@ function createPropertiesFromLog(log: ReadableLogRecord): [Properties, Measureme key === SEMATTRS_EXCEPTION_STACKTRACE ) ) { - properties[key] = log.attributes[key] as string; + properties[key] = serializeAttribute(log.attributes[key]); } } } diff --git a/sdk/monitor/monitor-opentelemetry-exporter/src/utils/spanUtils.ts b/sdk/monitor/monitor-opentelemetry-exporter/src/utils/spanUtils.ts index 88ab089782b9..5632fc4e7c1f 100644 --- a/sdk/monitor/monitor-opentelemetry-exporter/src/utils/spanUtils.ts +++ b/sdk/monitor/monitor-opentelemetry-exporter/src/utils/spanUtils.ts @@ -38,6 +38,7 @@ import { getUrl, hrTimeToDate, isSqlDB, + serializeAttribute, } from "./common"; import { Tags, Properties, MSLink, Measurements } from "../types"; import { parseEventHubSpan } from "./eventhub"; @@ -137,7 +138,7 @@ function createPropertiesFromSpanAttributes(attributes?: Attributes): { key === SEMATTRS_EXCEPTION_STACKTRACE ) ) { - properties[key] = attributes[key] as string; + properties[key] = serializeAttribute(attributes[key]); } } } diff --git a/sdk/monitor/monitor-opentelemetry-exporter/test/internal/commonUtils.test.ts b/sdk/monitor/monitor-opentelemetry-exporter/test/internal/commonUtils.test.ts index 412c21448624..abcd400c55f3 100644 --- a/sdk/monitor/monitor-opentelemetry-exporter/test/internal/commonUtils.test.ts +++ b/sdk/monitor/monitor-opentelemetry-exporter/test/internal/commonUtils.test.ts @@ -4,7 +4,7 @@ import os from "os"; import * as assert from "assert"; import { Resource } from "@opentelemetry/resources"; import { Tags } from "../../src/types"; -import { createTagsFromResource } from "../../src/utils/common"; +import { createTagsFromResource, serializeAttribute } from "../../src/utils/common"; describe("commonUtils.ts", () => { describe("#createTagsFromResource", () => { @@ -96,5 +96,21 @@ describe("commonUtils.ts", () => { const tags: Tags = createTagsFromResource(resource); assert.ok(tags["ai.cloud.role"].startsWith("unknown_service"), "wrong ai.cloud.role"); }); + + describe("#createProperties", () => { + it("should serialize attributes", () => { + let attr = serializeAttribute("test"); + assert.strictEqual(attr, "test"); + attr = serializeAttribute(false); + assert.strictEqual(attr, "false"); + attr = serializeAttribute("123"); + assert.strictEqual(attr, "123"); + attr = serializeAttribute({ test: "value" }); + assert.strictEqual(attr, '{"test":"value"}'); + attr = serializeAttribute(new Error("testError") as any); + assert.ok(attr.includes('"stack":"Error: testError')); + assert.ok(attr.includes('"message":"testError"')); + }); + }); }); }); diff --git a/sdk/monitor/monitor-opentelemetry/src/logs/batchLogRecordProcessor.ts b/sdk/monitor/monitor-opentelemetry/src/logs/batchLogRecordProcessor.ts index d9f4c46cd855..148b1e8f10fa 100644 --- a/sdk/monitor/monitor-opentelemetry/src/logs/batchLogRecordProcessor.ts +++ b/sdk/monitor/monitor-opentelemetry/src/logs/batchLogRecordProcessor.ts @@ -30,12 +30,6 @@ export class AzureBatchLogRecordProcessor extends BatchLogRecordProcessor { } } } - // Ensure nested log attributes are serialized - for (const [key, value] of Object.entries(logRecord.attributes)) { - if (typeof value === "object") { - logRecord.attributes[key] = JSON.stringify(value); - } - } super.onEmit(logRecord); } } diff --git a/sdk/monitor/monitor-opentelemetry/test/internal/unit/logs/batchLogRecordProcessor.test.ts b/sdk/monitor/monitor-opentelemetry/test/internal/unit/logs/batchLogRecordProcessor.test.ts index 9565bdb3ce88..5a46d5ec0854 100644 --- a/sdk/monitor/monitor-opentelemetry/test/internal/unit/logs/batchLogRecordProcessor.test.ts +++ b/sdk/monitor/monitor-opentelemetry/test/internal/unit/logs/batchLogRecordProcessor.test.ts @@ -78,28 +78,5 @@ describe("AzureBatchLogRecordProcessor", () => { span.end(); }); }); - - it("should serialize nested log attributes", () => { - const memoryLogExporter = new InMemoryLogRecordExporter(); - const processor = new AzureBatchLogRecordProcessor(memoryLogExporter, { - enableTraceBasedSamplingForLogs: false, - }); - const loggerProvider = new LoggerProvider(); - loggerProvider.addLogRecordProcessor(processor); - const sampler = new ApplicationInsightsSampler(1); - const tracerProvider = new NodeTracerProvider({ sampler: sampler }); - tracerProvider.getTracer("testTracere").startActiveSpan("test", async (span) => { - // Generate Log record - const logRecord: APILogRecord = { - attributes: { test: { nested: "value" } }, - body: "testRecord", - }; - loggerProvider.getLogger("testLoggere").emit(logRecord); - await loggerProvider.forceFlush(); - span.end(); - }); - const logRecords = memoryLogExporter.getFinishedLogRecords(); - assert.strictEqual(logRecords[0].attributes.test, '{"nested":"value"}'); - }); }); });