Skip to content

Commit 926f2b1

Browse files
author
deepshekhardas
committed
fix(core): escape dots in log attribute keys to prevent incorrect unflattening (#1510)
1 parent 9377289 commit 926f2b1

File tree

2 files changed

+72
-15
lines changed

2 files changed

+72
-15
lines changed

packages/core/src/v3/utils/flattenAttributes.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import { Attributes } from "@opentelemetry/api";
22

3+
function escapeKey(key: string): string {
4+
return key.replace(/\./g, "\\.");
5+
}
6+
7+
function unescapeKey(key: string): string {
8+
return key.replace(/\\\./g, ".");
9+
}
10+
11+
312
export const NULL_SENTINEL = "$@null((";
413
export const CIRCULAR_REFERENCE_SENTINEL = "$@circular((";
514

@@ -24,7 +33,7 @@ class AttributeFlattener {
2433
constructor(
2534
private maxAttributeCount?: number,
2635
private maxDepth: number = DEFAULT_MAX_DEPTH
27-
) {}
36+
) { }
2837

2938
get attributes(): Attributes {
3039
return this.result;
@@ -117,7 +126,8 @@ class AttributeFlattener {
117126
if (!this.canAddMoreAttributes()) break;
118127
// Use the key directly if it's a string, otherwise convert it
119128
const keyStr = typeof key === "string" ? key : String(key);
120-
this.#processValue(value, `${prefix || "map"}.${keyStr}`, depth);
129+
this.#processValue(value, `${prefix || "map"}.${escapeKey(keyStr)}`, depth);
130+
121131
}
122132
return;
123133
}
@@ -200,7 +210,9 @@ class AttributeFlattener {
200210
break;
201211
}
202212

203-
const newPrefix = `${prefix ? `${prefix}.` : ""}${Array.isArray(obj) ? `[${key}]` : key}`;
213+
const escapedKey = Array.isArray(obj) ? `[${key}]` : escapeKey(key);
214+
const newPrefix = `${prefix ? `${prefix}.` : ""}${escapedKey}`;
215+
204216

205217
if (Array.isArray(value)) {
206218
for (let i = 0; i < value.length; i++) {
@@ -278,25 +290,27 @@ export function unflattenAttributes(
278290
continue;
279291
}
280292

281-
const parts = key.split(".").reduce(
293+
const parts = key.split(/(?<!\\)\./).reduce(
282294
(acc, part) => {
283-
if (part.startsWith("[") && part.endsWith("]")) {
295+
const unescapedPart = unescapeKey(part);
296+
if (unescapedPart.startsWith("[") && unescapedPart.endsWith("]")) {
284297
// Handle array indices more precisely
285-
const match = part.match(/^\[(\d+)\]$/);
298+
const match = unescapedPart.match(/^\[(\d+)\]$/);
286299
if (match && match[1]) {
287300
acc.push(parseInt(match[1]));
288301
} else {
289302
// Remove brackets for non-numeric array keys
290-
acc.push(part.slice(1, -1));
303+
acc.push(unescapedPart.slice(1, -1));
291304
}
292305
} else {
293-
acc.push(part);
306+
acc.push(unescapedPart);
294307
}
295308
return acc;
296309
},
297310
[] as (string | number)[]
298311
);
299312

313+
300314
// Skip keys that exceed max depth to prevent memory exhaustion
301315
if (parts.length > maxDepth) {
302316
continue;

packages/core/test/flattenAttributes.test.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { describe, it, expect } from "vitest";
12
import { flattenAttributes, unflattenAttributes } from "../src/v3/utils/flattenAttributes.js";
23

4+
35
describe("flattenAttributes", () => {
46
it("handles number keys correctly", () => {
57
expect(flattenAttributes({ bar: { "25": "foo" } })).toEqual({ "bar.25": "foo" });
@@ -15,6 +17,19 @@ describe("flattenAttributes", () => {
1517
expect(unflattenAttributes({ "bar.25": "foo" })).toEqual({ bar: { 25: "foo" } });
1618
});
1719

20+
it("handles keys with periods correctly", () => {
21+
const obj = { "Key 0.002mm": 31.4 };
22+
const flattened = flattenAttributes(obj);
23+
expect(flattened).toEqual({ "Key 0\\.002mm": 31.4 });
24+
expect(unflattenAttributes(flattened)).toEqual(obj);
25+
26+
const nestedObj = { parent: { "child.key": "value" } };
27+
const nestedFlattened = flattenAttributes(nestedObj);
28+
expect(nestedFlattened).toEqual({ "parent.child\\.key": "value" });
29+
expect(unflattenAttributes(nestedFlattened)).toEqual(nestedObj);
30+
});
31+
32+
1833
it("handles null correctly", () => {
1934
expect(flattenAttributes(null)).toEqual({ "": "$@null((" });
2035
expect(unflattenAttributes({ "": "$@null((" })).toEqual(null);
@@ -297,9 +312,9 @@ describe("flattenAttributes", () => {
297312
});
298313

299314
it("handles function values correctly", () => {
300-
function namedFunction() {}
301-
const anonymousFunction = function () {};
302-
const arrowFunction = () => {};
315+
function namedFunction() { }
316+
const anonymousFunction = function () { };
317+
const arrowFunction = () => { };
303318

304319
const result = flattenAttributes({
305320
named: namedFunction,
@@ -317,7 +332,7 @@ describe("flattenAttributes", () => {
317332
it("handles mixed problematic types", () => {
318333
const complexObj = {
319334
error: new Error("Mixed error"),
320-
func: function testFunc() {},
335+
func: function testFunc() { },
321336
date: new Date("2023-01-01"),
322337
normal: "string",
323338
number: 42,
@@ -415,10 +430,10 @@ describe("flattenAttributes", () => {
415430
it("handles Promise objects correctly", () => {
416431
const resolvedPromise = Promise.resolve("value");
417432
const rejectedPromise = Promise.reject(new Error("failed"));
418-
const pendingPromise = new Promise(() => {}); // Never resolves
433+
const pendingPromise = new Promise(() => { }); // Never resolves
419434

420435
// Catch the rejection to avoid unhandled promise rejection warnings
421-
rejectedPromise.catch(() => {});
436+
rejectedPromise.catch(() => { });
422437

423438
const result = flattenAttributes({
424439
resolved: resolvedPromise,
@@ -481,7 +496,7 @@ describe("flattenAttributes", () => {
481496
it("handles complex mixed object with all special types", () => {
482497
const complexObj = {
483498
error: new Error("Test error"),
484-
func: function testFunc() {},
499+
func: function testFunc() { },
485500
date: new Date("2023-01-01"),
486501
mySet: new Set([1, 2, 3]),
487502
myMap: new Map([["key", "value"]]),
@@ -629,6 +644,34 @@ describe("unflattenAttributes", () => {
629644
});
630645
});
631646

647+
it("handles keys with periods correctly (literal keys vs. nested paths)", () => {
648+
const flattened = {
649+
"user.name": "John Doe",
650+
"user\\.email": "john.doe@example.com",
651+
"data.version": "1.0",
652+
"file.name.with\\.dots": "document.pdf",
653+
};
654+
655+
const expected = {
656+
user: {
657+
name: "John Doe",
658+
},
659+
"user.email": "john.doe@example.com",
660+
data: {
661+
version: "1.0",
662+
},
663+
file: {
664+
name: {
665+
"with.dots": "document.pdf",
666+
},
667+
},
668+
};
669+
670+
671+
expect(unflattenAttributes(flattened)).toEqual(expected);
672+
});
673+
674+
632675
it("respects maxDepth limit and skips overly deep keys", () => {
633676
// Create a flattened object with keys at various depths
634677
const flattened = {

0 commit comments

Comments
 (0)