Skip to content

Commit c128331

Browse files
committed
feat(logger): use async local storage for logger
1 parent 3886af3 commit c128331

File tree

12 files changed

+834
-200
lines changed

12 files changed

+834
-200
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { InvokeStore } from '@aws/lambda-invoke-store';
2+
import type { LogAttributes } from './types/logKeys.js';
3+
4+
/**
5+
* Manages storage of log attributes with automatic context detection.
6+
*
7+
* This class abstracts the storage mechanism for log attributes, automatically
8+
* choosing between AsyncLocalStorage (when in async context) and a fallback
9+
* object (when outside async context). The decision is made at runtime on
10+
* every method call to support Lambda's transition to async contexts.
11+
*/
12+
class LogAttributesStore {
13+
readonly #temporaryAttributesKey = Symbol(
14+
'powertools.logger.temporaryAttributes'
15+
);
16+
readonly #keysKey = Symbol('powertools.logger.keys');
17+
18+
#fallbackTemporaryAttributes: LogAttributes = {};
19+
readonly #fallbackKeys: Map<string, 'temp' | 'persistent'> = new Map();
20+
#persistentAttributes: LogAttributes = {};
21+
22+
#getTemporaryAttributes(): LogAttributes {
23+
if (InvokeStore.getContext() === undefined) {
24+
return this.#fallbackTemporaryAttributes;
25+
}
26+
27+
let stored = InvokeStore.get(this.#temporaryAttributesKey) as
28+
| LogAttributes
29+
| undefined;
30+
if (stored == null) {
31+
stored = {};
32+
InvokeStore.set(this.#temporaryAttributesKey, stored);
33+
}
34+
return stored;
35+
}
36+
37+
#getKeys(): Map<string, 'temp' | 'persistent'> {
38+
if (InvokeStore.getContext() === undefined) {
39+
return this.#fallbackKeys;
40+
}
41+
42+
let stored = InvokeStore.get(this.#keysKey) as
43+
| Map<string, 'temp' | 'persistent'>
44+
| undefined;
45+
if (stored == null) {
46+
stored = new Map();
47+
InvokeStore.set(this.#keysKey, stored);
48+
}
49+
return stored;
50+
}
51+
52+
public appendTemporaryKeys(attributes: LogAttributes): void {
53+
const tempAttrs = this.#getTemporaryAttributes();
54+
const keys = this.#getKeys();
55+
56+
for (const [key, value] of Object.entries(attributes)) {
57+
tempAttrs[key] = value;
58+
keys.set(key, 'temp');
59+
}
60+
}
61+
62+
public removeTemporaryKeys(keys: string[]): void {
63+
const tempAttrs = this.#getTemporaryAttributes();
64+
const keysMap = this.#getKeys();
65+
66+
for (const key of keys) {
67+
tempAttrs[key] = undefined;
68+
69+
if (this.#persistentAttributes[key]) {
70+
keysMap.set(key, 'persistent');
71+
} else {
72+
keysMap.delete(key);
73+
}
74+
}
75+
}
76+
77+
public getTemporaryAttributes(): LogAttributes {
78+
return { ...this.#getTemporaryAttributes() };
79+
}
80+
81+
public clearTemporaryAttributes(): void {
82+
const tempAttrs = this.#getTemporaryAttributes();
83+
const keysMap = this.#getKeys();
84+
85+
for (const key of Object.keys(tempAttrs)) {
86+
if (this.#persistentAttributes[key]) {
87+
keysMap.set(key, 'persistent');
88+
} else {
89+
keysMap.delete(key);
90+
}
91+
}
92+
93+
if (InvokeStore.getContext() === undefined) {
94+
this.#fallbackTemporaryAttributes = {};
95+
return;
96+
}
97+
98+
InvokeStore.set(this.#temporaryAttributesKey, {});
99+
}
100+
101+
public setPersistentAttributes(attributes: LogAttributes): void {
102+
const keysMap = this.#getKeys();
103+
this.#persistentAttributes = { ...attributes };
104+
105+
for (const key of Object.keys(attributes)) {
106+
keysMap.set(key, 'persistent');
107+
}
108+
}
109+
110+
public getPersistentAttributes(): LogAttributes {
111+
return { ...this.#persistentAttributes };
112+
}
113+
114+
public getAllAttributes(): LogAttributes {
115+
const result: LogAttributes = {};
116+
const tempAttrs = this.#getTemporaryAttributes();
117+
const keysMap = this.#getKeys();
118+
119+
// First add all persistent attributes
120+
for (const [key, value] of Object.entries(this.#persistentAttributes)) {
121+
if (value !== undefined) {
122+
result[key] = value;
123+
}
124+
}
125+
126+
// Then override with temporary attributes based on keysMap
127+
for (const [key, type] of keysMap.entries()) {
128+
if (type === 'temp' && tempAttrs[key] !== undefined) {
129+
result[key] = tempAttrs[key];
130+
}
131+
}
132+
133+
return result;
134+
}
135+
136+
public removePersistentKeys(keys: string[]): void {
137+
const keysMap = this.#getKeys();
138+
const tempAttrs = this.#getTemporaryAttributes();
139+
140+
for (const key of keys) {
141+
this.#persistentAttributes[key] = undefined;
142+
143+
if (tempAttrs[key]) {
144+
keysMap.set(key, 'temp');
145+
} else {
146+
keysMap.delete(key);
147+
}
148+
}
149+
}
150+
}
151+
152+
export { LogAttributesStore };

packages/logger/src/Logger.ts

Lines changed: 29 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
import type { LogFormatter } from './formatter/LogFormatter.js';
2525
import type { LogItem } from './formatter/LogItem.js';
2626
import { PowertoolsLogFormatter } from './formatter/PowertoolsLogFormatter.js';
27+
import { LogAttributesStore } from './LogAttributesStore.js';
2728
import { CircularMap } from './logBuffer.js';
2829
import type { ConfigServiceInterface } from './types/ConfigServiceInterface.js';
2930
import type {
@@ -142,20 +143,16 @@ class Logger extends Utility implements LoggerInterface {
142143
* environment variable AWS_LAMBDA_LOG_LEVEL is set
143144
*/
144145
#alcLogLevel?: Uppercase<LogLevel>;
145-
/**
146-
* Persistent log attributes that will be logged in all log items.
147-
*/
148-
private persistentLogAttributes: LogAttributes = {};
149146
/**
150147
* Standard attributes managed by Powertools that will be logged in all log items.
151148
*/
152149
private readonly powertoolsLogData: PowertoolsLogData = <PowertoolsLogData>{
153150
sampleRateValue: 0,
154151
};
155152
/**
156-
* Temporary log attributes that can be appended with `appendKeys()` method.
153+
* Store for managing temporary and persistent log attributes.
157154
*/
158-
private temporaryLogAttributes: LogKeys = {};
155+
readonly #attributesStore = new LogAttributesStore();
159156
/**
160157
* Buffer used to store logs until the logger is initialized.
161158
*
@@ -170,13 +167,6 @@ class Logger extends Utility implements LoggerInterface {
170167
* Flag used to determine if the logger is initialized.
171168
*/
172169
readonly #isInitialized: boolean = false;
173-
/**
174-
* Map used to hold the list of keys and their type.
175-
*
176-
* Because keys of different types can be overwritten, we keep a list of keys that were added and their last
177-
* type. We then use this map at log preparation time to pick the last one.
178-
*/
179-
readonly #keys: Map<string, 'temp' | 'persistent'> = new Map();
180170
/**
181171
* This is the initial log leval as set during the initialization of the logger.
182172
*
@@ -345,7 +335,8 @@ class Logger extends Utility implements LoggerInterface {
345335
logFormatter: this.getLogFormatter(),
346336
customConfigService: this.getCustomConfigService(),
347337
environment: this.powertoolsLogData.environment,
348-
persistentLogAttributes: this.persistentLogAttributes,
338+
persistentLogAttributes:
339+
this.#attributesStore.getPersistentAttributes(),
349340
jsonReplacerFn: this.#jsonReplacerFn,
350341
correlationIdSearchFn: this.#correlationIdSearchFn,
351342
...(this.#bufferConfig.enabled && {
@@ -365,8 +356,9 @@ class Logger extends Utility implements LoggerInterface {
365356
childLogger.addContext(
366357
this.powertoolsLogData.lambdaContext as unknown as Context
367358
);
368-
if (this.temporaryLogAttributes) {
369-
childLogger.appendKeys(this.temporaryLogAttributes);
359+
const temporaryAttributes = this.#attributesStore.getTemporaryAttributes();
360+
if (Object.keys(temporaryAttributes).length > 0) {
361+
childLogger.appendKeys(temporaryAttributes);
370362
}
371363

372364
return childLogger;
@@ -431,7 +423,7 @@ class Logger extends Utility implements LoggerInterface {
431423
* that will be logged in all log items.
432424
*/
433425
public getPersistentLogAttributes(): LogAttributes {
434-
return this.persistentLogAttributes;
426+
return this.#attributesStore.getPersistentAttributes();
435427
}
436428

437429
/**
@@ -604,15 +596,7 @@ class Logger extends Utility implements LoggerInterface {
604596
* @param keys - The keys to remove.
605597
*/
606598
public removeKeys(keys: string[]): void {
607-
for (const key of keys) {
608-
this.temporaryLogAttributes[key] = undefined;
609-
610-
if (this.persistentLogAttributes[key]) {
611-
this.#keys.set(key, 'persistent');
612-
} else {
613-
this.#keys.delete(key);
614-
}
615-
}
599+
this.#attributesStore.removeTemporaryKeys(keys);
616600
}
617601

618602
/**
@@ -634,15 +618,7 @@ class Logger extends Utility implements LoggerInterface {
634618
* @param keys - The keys to remove from the persistent attributes.
635619
*/
636620
public removePersistentKeys(keys: string[]): void {
637-
for (const key of keys) {
638-
this.persistentLogAttributes[key] = undefined;
639-
640-
if (this.temporaryLogAttributes[key]) {
641-
this.#keys.set(key, 'temp');
642-
} else {
643-
this.#keys.delete(key);
644-
}
645-
}
621+
this.#attributesStore.removePersistentKeys(keys);
646622
}
647623

648624
/**
@@ -656,14 +632,7 @@ class Logger extends Utility implements LoggerInterface {
656632
* Remove all temporary log attributes added with {@link appendKeys() `appendKeys()`} method.
657633
*/
658634
public resetKeys(): void {
659-
for (const key of Object.keys(this.temporaryLogAttributes)) {
660-
if (this.persistentLogAttributes[key]) {
661-
this.#keys.set(key, 'persistent');
662-
} else {
663-
this.#keys.delete(key);
664-
}
665-
}
666-
this.temporaryLogAttributes = {};
635+
this.#attributesStore.clearTemporaryAttributes();
667636
}
668637

669638
/**
@@ -687,7 +656,14 @@ class Logger extends Utility implements LoggerInterface {
687656
* @deprecated This method is deprecated and will be removed in the future major versions, please use {@link appendPersistentKeys() `appendPersistentKeys()`} instead.
688657
*/
689658
public setPersistentLogAttributes(attributes: LogKeys): void {
690-
this.persistentLogAttributes = attributes;
659+
this.#attributesStore.setPersistentAttributes(attributes);
660+
}
661+
662+
/**
663+
* @deprecated Use getPersistentLogAttributes() instead
664+
*/
665+
public get persistentLogAttributes(): LogKeys {
666+
return this.#attributesStore.getPersistentAttributes();
691667
}
692668

693669
/**
@@ -810,15 +786,17 @@ class Logger extends Utility implements LoggerInterface {
810786
* @param type - The type of the attributes to add.
811787
*/
812788
#appendKeys(attributes: LogKeys, type: 'temp' | 'persistent'): void {
813-
for (const attributeKey of Object.keys(attributes)) {
814-
if (this.#checkReservedKeyAndWarn(attributeKey) === false) {
815-
this.#keys.set(attributeKey, type);
789+
const filtered: LogKeys = {};
790+
for (const [key, value] of Object.entries(attributes)) {
791+
if (!this.#checkReservedKeyAndWarn(key)) {
792+
filtered[key] = value;
816793
}
817794
}
818795
if (type === 'temp') {
819-
merge(this.temporaryLogAttributes, attributes);
796+
this.#attributesStore.appendTemporaryKeys(filtered);
820797
} else {
821-
merge(this.persistentLogAttributes, attributes);
798+
const current = this.#attributesStore.getPersistentAttributes();
799+
this.#attributesStore.setPersistentAttributes(merge(current, filtered));
822800
}
823801
}
824802

@@ -875,18 +853,7 @@ class Logger extends Utility implements LoggerInterface {
875853
* Create additional attributes from persistent and temporary keys
876854
*/
877855
#createAdditionalAttributes(): LogAttributes {
878-
const attributes: LogAttributes = {};
879-
880-
for (const [key, type] of this.#keys) {
881-
if (!this.#checkReservedKeyAndWarn(key)) {
882-
attributes[key] =
883-
type === 'persistent'
884-
? this.persistentLogAttributes[key]
885-
: this.temporaryLogAttributes[key];
886-
}
887-
}
888-
889-
return attributes;
856+
return this.#attributesStore.getAllAttributes();
890857
}
891858

892859
/**
@@ -1568,7 +1535,7 @@ class Logger extends Utility implements LoggerInterface {
15681535
* Get the correlation ID from the log attributes.
15691536
*/
15701537
public getCorrelationId(): unknown {
1571-
return this.temporaryLogAttributes.correlation_id;
1538+
return this.#attributesStore.getTemporaryAttributes().correlation_id;
15721539
}
15731540
}
15741541

0 commit comments

Comments
 (0)