Skip to content

Commit 3848c37

Browse files
authored
Add more metrics for datastores and exception metrics (#130)
1 parent 5ee0241 commit 3848c37

File tree

6 files changed

+108
-35
lines changed

6 files changed

+108
-35
lines changed

src/datastore/LMDB.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { join } from 'path';
33
import { open, Database, RootDatabase } from 'lmdb';
44
import { LoggerFactory } from '../telemetry/LoggerFactory';
55
import { ScopedTelemetry } from '../telemetry/ScopedTelemetry';
6-
import { Measure, Telemetry } from '../telemetry/TelemetryDecorator';
6+
import { Telemetry } from '../telemetry/TelemetryDecorator';
7+
import { TelemetryService } from '../telemetry/TelemetryService';
78
import { pathToArtifact } from '../utils/ArtifactsDir';
89
import { extractErrorMessage } from '../utils/Errors';
910
import { DataStore, DataStoreFactory } from './DataStore';
@@ -12,15 +13,23 @@ import { encryptionStrategy } from './lmdb/Utils';
1213
const log = LoggerFactory.getLogger('LMDB');
1314

1415
export class LMDBStore implements DataStore {
15-
constructor(private readonly store: Database<unknown, string>) {}
16+
private readonly telemetry: ScopedTelemetry;
17+
18+
constructor(
19+
private readonly name: string,
20+
private readonly store: Database<unknown, string>,
21+
) {
22+
this.telemetry = TelemetryService.instance.get(`LMDB.${name}`);
23+
}
1624

1725
get<T>(key: string): T | undefined {
1826
return this.store.get(key) as T | undefined;
1927
}
2028

21-
@Measure({ name: 'put' })
2229
put<T>(key: string, value: T): Promise<boolean> {
23-
return this.store.put(key, value);
30+
return this.telemetry.measureAsync('put', () => {
31+
return this.store.put(key, value);
32+
});
2433
}
2534

2635
remove(key: string): Promise<boolean> {
@@ -41,7 +50,7 @@ export class LMDBStore implements DataStore {
4150
}
4251

4352
export class LMDBStoreFactory implements DataStoreFactory {
44-
@Telemetry() private readonly telemetry!: ScopedTelemetry;
53+
@Telemetry({ scope: 'LMDB.Global' }) private readonly telemetry!: ScopedTelemetry;
4554

4655
private readonly rootDir = pathToArtifact('lmdb');
4756
private readonly storePath = join(this.rootDir, Version);
@@ -82,7 +91,7 @@ export class LMDBStoreFactory implements DataStoreFactory {
8291
if (database === undefined) {
8392
throw new Error(`Failed to open LMDB store ${store}`);
8493
}
85-
val = new LMDBStore(database);
94+
val = new LMDBStore(store, database);
8695
this.stores.set(store, val);
8796
}
8897

src/datastore/MemoryStore.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1+
import { ScopedTelemetry } from '../telemetry/ScopedTelemetry';
2+
import { Telemetry } from '../telemetry/TelemetryDecorator';
3+
import { TelemetryService } from '../telemetry/TelemetryService';
14
import { DataStore, DataStoreFactory } from './DataStore';
25

36
export class MemoryStore implements DataStore {
47
private readonly store = new Map<string, unknown>();
8+
private readonly telemetry: ScopedTelemetry;
9+
10+
constructor(private readonly name: string) {
11+
this.telemetry = TelemetryService.instance.get(`MemoryStore.${name}`);
12+
}
513

614
get<T>(key: string): T | undefined {
715
const val = this.store.get(key);
@@ -10,8 +18,10 @@ export class MemoryStore implements DataStore {
1018
}
1119

1220
put<T>(key: string, value: T): Promise<boolean> {
13-
this.store.set(key, value);
14-
return Promise.resolve(true);
21+
return this.telemetry.measureAsync('put', () => {
22+
this.store.set(key, value);
23+
return Promise.resolve(true);
24+
});
1525
}
1626

1727
remove(key: string): Promise<boolean> {
@@ -35,12 +45,18 @@ export class MemoryStore implements DataStore {
3545
}
3646

3747
export class MemoryStoreFactory implements DataStoreFactory {
48+
@Telemetry({ scope: 'MemoryStore.Global' }) private readonly telemetry!: ScopedTelemetry;
49+
3850
private readonly stores = new Map<string, MemoryStore>();
3951

52+
constructor() {
53+
this.registerMemoryStoreGauges();
54+
}
55+
4056
getOrCreate(store: string): DataStore {
4157
let val = this.stores.get(store);
4258
if (val === undefined) {
43-
val = new MemoryStore();
59+
val = new MemoryStore(store);
4460
this.stores.set(store, val);
4561
}
4662

@@ -69,4 +85,19 @@ export class MemoryStoreFactory implements DataStoreFactory {
6985
close(): Promise<void> {
7086
return Promise.resolve();
7187
}
88+
89+
private registerMemoryStoreGauges(): void {
90+
this.telemetry.registerGaugeProvider('stores.count', () => this.stores.size, { unit: '1' });
91+
this.telemetry.registerGaugeProvider(
92+
'global.entries',
93+
() => {
94+
let total = 0;
95+
for (const store of this.stores.values()) {
96+
total += store.keys().length;
97+
}
98+
return total;
99+
},
100+
{ unit: '1' },
101+
);
102+
}
72103
}

src/telemetry/TelemetryService.ts

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { MetricReader } from '@opentelemetry/sdk-metrics';
33
import { NodeSDK } from '@opentelemetry/sdk-node';
44
import { v4 } from 'uuid';
55
import { Closeable } from '../utils/Closeable';
6+
import { extractLocationFromStack } from '../utils/Errors';
67
import { LoggerFactory } from './LoggerFactory';
78
import { otelSdk } from './OTELInstrumentation';
89
import { ScopedTelemetry } from './ScopedTelemetry';
@@ -149,39 +150,38 @@ export class TelemetryService implements Closeable {
149150
}
150151

151152
private registerErrorHandlers(telemetry: ScopedTelemetry): void {
152-
process.on('uncaughtExceptionMonitor', (error, origin) => {
153-
this.logger.error(
153+
process.on('unhandledRejection', (reason, _promise) => {
154+
this.logger.error(reason, 'Unhandled promise rejection');
155+
156+
const location = reason instanceof Error ? extractLocationFromStack(reason.stack) : {};
157+
telemetry.count(
158+
'process.promise.unhandled',
159+
1,
160+
{ unit: '1' },
154161
{
155-
error,
156-
origin,
162+
'error.type': reason instanceof Error ? reason.name : typeof reason,
163+
...location,
157164
},
158-
'Uncaught exception monitor',
159165
);
160166

161-
telemetry.count('process.exception.monitor.uncaught', 1, { unit: '1' });
162-
});
163-
164-
process.on('unhandledRejection', (reason, promise) => {
165-
this.logger.error(
166-
{
167-
reason,
168-
promise,
169-
},
170-
'Unhandled promise rejection',
171-
);
172-
173-
telemetry.count('process.promise.unhandled', 1, { unit: '1' });
167+
void this.metricsReader?.forceFlush();
174168
});
175169

176170
process.on('uncaughtException', (error, origin) => {
177-
this.logger.error(
171+
this.logger.error(error, `Uncaught exception ${origin}`);
172+
173+
telemetry.count(
174+
'process.exception.uncaught',
175+
1,
176+
{ unit: '1' },
178177
{
179-
error,
180-
origin,
178+
'error.type': error.name,
179+
'error.origin': origin,
180+
...extractLocationFromStack(error.stack),
181181
},
182-
'Uncaught exception',
183182
);
184-
telemetry.count('process.exception.uncaught', 1, { unit: '1' });
183+
184+
void this.metricsReader?.forceFlush();
185185
});
186186
}
187187

src/utils/ArtifactsDir.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { existsSync, mkdirSync } from 'fs';
22
import { resolve, join } from 'path';
33

4-
const RELATIVE_ROOT_DIR = './.aws-cfn-storage';
4+
const RELATIVE_ROOT_DIR = '.aws-cfn-storage';
55

66
/**
77
* This will create artifacts in the directory where the app is executing
@@ -10,7 +10,12 @@ const RELATIVE_ROOT_DIR = './.aws-cfn-storage';
1010
*/
1111
function getOrCreateAbsolutePath(artifactDir: string | undefined = undefined): string {
1212
const dir = resolve(__dirname);
13-
const path = join(dir, RELATIVE_ROOT_DIR, artifactDir ?? '.');
13+
let path: string;
14+
if (artifactDir) {
15+
path = join(dir, RELATIVE_ROOT_DIR, artifactDir);
16+
} else {
17+
path = join(dir, RELATIVE_ROOT_DIR);
18+
}
1419

1520
if (existsSync(path)) {
1621
return path;

src/utils/Errors.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,31 @@ export function extractErrorMessage(error: unknown) {
88

99
return toString(error);
1010
}
11+
12+
/**
13+
* Best effort extraction of location of exception based on stack trace
14+
*/
15+
export function extractLocationFromStack(stack?: string): {
16+
'error.file'?: string;
17+
'error.line'?: number;
18+
'error.column'?: number;
19+
} {
20+
if (!stack) return {};
21+
22+
// Match first line with file location: at ... (/path/to/file.ts:line:column)
23+
const match = stack.match(/at .+\((.+):(\d+):(\d+)\)|at (.+):(\d+):(\d+)/);
24+
if (!match) return {};
25+
26+
const fullPath = match[1] || match[4];
27+
const line = parseInt(match[2] || match[5], 10); // eslint-disable-line unicorn/prefer-number-properties
28+
const column = parseInt(match[3] || match[6], 10); // eslint-disable-line unicorn/prefer-number-properties
29+
30+
// Extract only filename without path
31+
const filename = fullPath?.split('/').pop()?.split('\\').pop();
32+
33+
return {
34+
'error.file': filename,
35+
'error.line': line,
36+
'error.column': column,
37+
};
38+
}

tst/unit/schema/GetSchemaTask.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe('GetSchemaTask', () => {
2121

2222
beforeEach(() => {
2323
vi.clearAllMocks();
24-
mockDataStore = new MemoryStore();
24+
mockDataStore = new MemoryStore('TestStore');
2525
mockLogger = stubInterface<Logger>();
2626

2727
// Setup Sinon stubs

0 commit comments

Comments
 (0)