@devopsplaybook.io/common-utils 1.0.0-beta.5.9c5ecde
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/main-build.yml +17 -0
- package/.github/workflows/npm-upgrade.yml +16 -0
- package/.github/workflows/pr-check.yml +26 -0
- package/.github/workflows/reusable-merge-build.yml +141 -0
- package/.github/workflows/reusable-npm-merge.yml +135 -0
- package/.github/workflows/reusable-npm-pr.yml +153 -0
- package/.github/workflows/reusable-npm-upgrade.yml +92 -0
- package/.github/workflows/reusable-pr-verify.yml +135 -0
- package/README.md +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +24 -0
- package/dist/src/ConfigBase.d.ts +104 -0
- package/dist/src/ConfigBase.js +201 -0
- package/dist/src/DbUtils.d.ts +50 -0
- package/dist/src/DbUtils.js +117 -0
- package/dist/src/DbUtilsNoTelemetry.d.ts +27 -0
- package/dist/src/DbUtilsNoTelemetry.js +89 -0
- package/dist/src/OTelContext.d.ts +35 -0
- package/dist/src/OTelContext.js +42 -0
- package/dist/src/PostgresDbUtils.d.ts +52 -0
- package/dist/src/PostgresDbUtils.js +217 -0
- package/dist/src/SqlDbUtils.d.ts +40 -0
- package/dist/src/SqlDbUtils.js +156 -0
- package/dist/src/SystemCommand.d.ts +9 -0
- package/dist/src/SystemCommand.js +56 -0
- package/dist/src/Timeout.d.ts +6 -0
- package/dist/src/Timeout.js +15 -0
- package/eslint.config.mjs +10 -0
- package/index.ts +8 -0
- package/jest.config.js +13 -0
- package/package.json +50 -0
- package/prettierrc.json +5 -0
- package/src/ConfigBase.spec.ts +108 -0
- package/src/ConfigBase.ts +213 -0
- package/src/DbUtils.spec.ts +23 -0
- package/src/DbUtils.ts +118 -0
- package/src/DbUtilsNoTelemetry.spec.ts +174 -0
- package/src/DbUtilsNoTelemetry.ts +121 -0
- package/src/OTelContext.spec.ts +58 -0
- package/src/OTelContext.ts +65 -0
- package/src/PostgresDbUtils.spec.ts +155 -0
- package/src/PostgresDbUtils.ts +233 -0
- package/src/SqlDbUtils.spec.ts +111 -0
- package/src/SqlDbUtils.ts +153 -0
- package/src/SystemCommand.spec.ts +18 -0
- package/src/SystemCommand.ts +23 -0
- package/src/Timeout.spec.ts +18 -0
- package/src/Timeout.ts +12 -0
- package/tsconfig.json +14 -0
- package/tsconfig.spec.json +7 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DbUtilsNoTelemetryBatchInsert,
|
|
3
|
+
DbUtilsNoTelemetryExecSQL,
|
|
4
|
+
DbUtilsNoTelemetryQuerySQL,
|
|
5
|
+
} from "./DbUtilsNoTelemetry";
|
|
6
|
+
|
|
7
|
+
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
|
8
|
+
|
|
9
|
+
// Shared mock DB handle – defined BEFORE jest.mock factory so it's hoisted correctly.
|
|
10
|
+
// We use jest.fn() at module scope; the mock factory captures the same reference.
|
|
11
|
+
const mockPrepare = jest.fn(() => ({
|
|
12
|
+
run: jest.fn().mockReturnValue({ changes: 0 }),
|
|
13
|
+
all: jest.fn().mockReturnValue([]),
|
|
14
|
+
}));
|
|
15
|
+
const mockQuery = jest.fn();
|
|
16
|
+
const mockDbHandle = { prepare: mockPrepare, query: mockQuery };
|
|
17
|
+
|
|
18
|
+
let currentDbType: "sqlite" | "postgres" = "sqlite";
|
|
19
|
+
|
|
20
|
+
jest.mock("./DbUtils", () => ({
|
|
21
|
+
DbUtilsGetDatabase: jest.fn(() => mockDbHandle),
|
|
22
|
+
DbUtilsGetType: jest.fn(() => currentDbType),
|
|
23
|
+
convertToPostgresPlaceholders: jest.fn((sql: string) => {
|
|
24
|
+
let idx = 1;
|
|
25
|
+
return sql.replace(/\?/g, () => `$${idx++}`);
|
|
26
|
+
}),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
jest.mock("@devopsplaybook.io/otel-utils", () => ({
|
|
30
|
+
ModuleLogger: jest.fn(),
|
|
31
|
+
StandardLogger: jest.fn(),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
import * as DbUtilsNoTelemetryModule from "./DbUtilsNoTelemetry";
|
|
35
|
+
|
|
36
|
+
beforeAll(() => {
|
|
37
|
+
const mockLogger = {
|
|
38
|
+
error: jest.fn(),
|
|
39
|
+
info: jest.fn(),
|
|
40
|
+
warn: jest.fn(),
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
} as any;
|
|
43
|
+
DbUtilsNoTelemetryModule.DbUtilsNoTelemetrySetLogger({
|
|
44
|
+
createModuleLogger: () => mockLogger,
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
46
|
+
} as any);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
jest.clearAllMocks();
|
|
51
|
+
currentDbType = "sqlite";
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("DbUtilsNoTelemetryBatchInsert", () => {
|
|
55
|
+
it("returns 0 for empty rows", () => {
|
|
56
|
+
const result = DbUtilsNoTelemetryBatchInsert("INTO t (c)", 1, []);
|
|
57
|
+
expect(result).toBe(0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("generates correct multi-row VALUES SQL (sqlite)", () => {
|
|
61
|
+
currentDbType = "sqlite";
|
|
62
|
+
mockPrepare.mockReturnValue({
|
|
63
|
+
run: jest.fn().mockReturnValue({ changes: 2 }),
|
|
64
|
+
all: jest.fn(),
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
66
|
+
} as any);
|
|
67
|
+
|
|
68
|
+
const rows = [
|
|
69
|
+
["a1", "b1"],
|
|
70
|
+
["a2", "b2"],
|
|
71
|
+
];
|
|
72
|
+
const result = DbUtilsNoTelemetryBatchInsert("INTO t (c1,c2)", 2, rows);
|
|
73
|
+
expect(result).toBe(2);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("DbUtilsNoTelemetryExecSQL (sqlite)", () => {
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
currentDbType = "sqlite";
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("resolves with changes count on success", () => {
|
|
83
|
+
mockPrepare.mockReturnValue({
|
|
84
|
+
run: jest.fn().mockReturnValue({ changes: 3 }),
|
|
85
|
+
all: jest.fn(),
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
87
|
+
} as any);
|
|
88
|
+
|
|
89
|
+
const result = DbUtilsNoTelemetryExecSQL("INSERT INTO t (c) VALUES (?)", [
|
|
90
|
+
"x",
|
|
91
|
+
]);
|
|
92
|
+
expect(result).toBe(3);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("DbUtilsNoTelemetryExecSQL (postgres)", () => {
|
|
97
|
+
beforeEach(() => {
|
|
98
|
+
currentDbType = "postgres";
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("resolves with rowCount on success", async () => {
|
|
102
|
+
mockQuery.mockImplementation(
|
|
103
|
+
(_sql: string, _params: unknown[], cb: Function) => {
|
|
104
|
+
cb(null, { rowCount: 5 });
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const result = await DbUtilsNoTelemetryExecSQL(
|
|
109
|
+
"INSERT INTO t (c) VALUES (?)",
|
|
110
|
+
["x"],
|
|
111
|
+
);
|
|
112
|
+
expect(result).toBe(5);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("rejects on error", async () => {
|
|
116
|
+
mockQuery.mockImplementation(
|
|
117
|
+
(_sql: string, _params: unknown[], cb: Function) => {
|
|
118
|
+
cb(new Error("deadlock detected"));
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
await expect(
|
|
123
|
+
DbUtilsNoTelemetryExecSQL("INSERT INTO t (c) VALUES (?)", ["x"]),
|
|
124
|
+
).rejects.toThrow("deadlock detected");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("DbUtilsNoTelemetryQuerySQL (sqlite)", () => {
|
|
129
|
+
beforeEach(() => {
|
|
130
|
+
currentDbType = "sqlite";
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("returns rows on success", () => {
|
|
134
|
+
const expectedRows = [{ id: 1 }, { id: 2 }];
|
|
135
|
+
mockPrepare.mockReturnValue({
|
|
136
|
+
run: jest.fn(),
|
|
137
|
+
all: jest.fn().mockReturnValue(expectedRows),
|
|
138
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
139
|
+
} as any);
|
|
140
|
+
|
|
141
|
+
const result = DbUtilsNoTelemetryQuerySQL("SELECT * FROM t");
|
|
142
|
+
expect(result).toEqual(expectedRows);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("DbUtilsNoTelemetryQuerySQL (postgres)", () => {
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
currentDbType = "postgres";
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("returns rows on success", async () => {
|
|
152
|
+
const expectedRows = [{ id: 1 }, { id: 2 }];
|
|
153
|
+
mockQuery.mockImplementation(
|
|
154
|
+
(_sql: string, _params: unknown[], cb: Function) => {
|
|
155
|
+
cb(null, { rows: expectedRows });
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const result = await DbUtilsNoTelemetryQuerySQL("SELECT * FROM t");
|
|
160
|
+
expect(result).toEqual(expectedRows);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("rejects on error", async () => {
|
|
164
|
+
mockQuery.mockImplementation(
|
|
165
|
+
(_sql: string, _params: unknown[], cb: Function) => {
|
|
166
|
+
cb(new Error("connection lost"));
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
await expect(DbUtilsNoTelemetryQuerySQL("SELECT * FROM t")).rejects.toThrow(
|
|
171
|
+
"connection lost",
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { StandardLogger, ModuleLogger } from "@devopsplaybook.io/otel-utils";
|
|
2
|
+
import {
|
|
3
|
+
DbUtilsGetDatabase,
|
|
4
|
+
DbUtilsGetType,
|
|
5
|
+
convertToPostgresPlaceholders,
|
|
6
|
+
} from "./DbUtils";
|
|
7
|
+
|
|
8
|
+
let logger: ModuleLogger;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Injects the OTel logger instance used by no-telemetry DB operations.
|
|
12
|
+
* Must be called once at startup.
|
|
13
|
+
*/
|
|
14
|
+
export function DbUtilsNoTelemetrySetLogger(loggerIn: StandardLogger): void {
|
|
15
|
+
logger = loggerIn.createModuleLogger("DbUtilsNoTelemetry");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Execute a multi-row INSERT with a flat parameter array.
|
|
20
|
+
* Builds: INSERT INTO <tableCols> VALUES (?,?...),(?,?...),...
|
|
21
|
+
*
|
|
22
|
+
* @returns Number of rows inserted.
|
|
23
|
+
*/
|
|
24
|
+
export function DbUtilsNoTelemetryBatchInsert(
|
|
25
|
+
tableCols: string,
|
|
26
|
+
numCols: number,
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
rows: any[][],
|
|
29
|
+
): number | Promise<number> {
|
|
30
|
+
if (rows.length === 0) return 0;
|
|
31
|
+
const rowSQL = `(${Array.from({ length: numCols }, () => "?").join(",")})`;
|
|
32
|
+
const multiValues = Array.from({ length: rows.length }, () => rowSQL).join(
|
|
33
|
+
",",
|
|
34
|
+
);
|
|
35
|
+
const sql = `INSERT ${tableCols} VALUES ${multiValues}`;
|
|
36
|
+
return DbUtilsNoTelemetryExecSQL(sql, rows.flat());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Execute a write SQL statement **without** creating an OTel span.
|
|
41
|
+
* Use this on high-throughput paths where span overhead matters.
|
|
42
|
+
*
|
|
43
|
+
* @returns Number of rows changed.
|
|
44
|
+
*/
|
|
45
|
+
export function DbUtilsNoTelemetryExecSQL(
|
|
46
|
+
sql: string,
|
|
47
|
+
params: unknown[] = [],
|
|
48
|
+
): number | Promise<number> {
|
|
49
|
+
const dbType = DbUtilsGetType();
|
|
50
|
+
if (dbType === "postgres") {
|
|
51
|
+
const pgSql = convertToPostgresPlaceholders(sql);
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
(DbUtilsGetDatabase() as any).query(
|
|
55
|
+
pgSql,
|
|
56
|
+
params,
|
|
57
|
+
(error: Error | null, result: { rowCount: number | null }) => {
|
|
58
|
+
if (error) {
|
|
59
|
+
logger.error(`SQL INSERT ERROR: ${sql.substring(0, 200)}`, error);
|
|
60
|
+
reject(error);
|
|
61
|
+
} else {
|
|
62
|
+
resolve(result.rowCount || 0);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
// SQLite (better-sqlite3) – synchronous
|
|
69
|
+
const stmt = (
|
|
70
|
+
DbUtilsGetDatabase() as {
|
|
71
|
+
prepare: (sql: string) => {
|
|
72
|
+
run: (params: unknown[]) => { changes: number };
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
).prepare(sql);
|
|
76
|
+
const result = stmt.run(params);
|
|
77
|
+
return result.changes;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Execute a read SQL query **without** creating an OTel span.
|
|
82
|
+
* Use this on high-throughput paths where span overhead matters.
|
|
83
|
+
*
|
|
84
|
+
* @returns Array of row objects.
|
|
85
|
+
*/
|
|
86
|
+
export function DbUtilsNoTelemetryQuerySQL(
|
|
87
|
+
sql: string,
|
|
88
|
+
params: unknown[] = [],
|
|
89
|
+
debug = false,
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
91
|
+
): any[] | Promise<any[]> {
|
|
92
|
+
if (debug) {
|
|
93
|
+
console.log(sql);
|
|
94
|
+
}
|
|
95
|
+
const dbType = DbUtilsGetType();
|
|
96
|
+
if (dbType === "postgres") {
|
|
97
|
+
const pgSql = convertToPostgresPlaceholders(sql);
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
100
|
+
(DbUtilsGetDatabase() as any).query(
|
|
101
|
+
pgSql,
|
|
102
|
+
params,
|
|
103
|
+
(error: Error | null, result: { rows: unknown[] }) => {
|
|
104
|
+
if (error) {
|
|
105
|
+
logger.error(`SQL ERROR: ${sql}`, error);
|
|
106
|
+
reject(error);
|
|
107
|
+
} else {
|
|
108
|
+
resolve(result.rows);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// SQLite (better-sqlite3) – synchronous
|
|
115
|
+
const stmt = (
|
|
116
|
+
DbUtilsGetDatabase() as {
|
|
117
|
+
prepare: (sql: string) => { all: (params: unknown[]) => unknown[] };
|
|
118
|
+
}
|
|
119
|
+
).prepare(sql);
|
|
120
|
+
return stmt.all(params);
|
|
121
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createOTelContext } from "./OTelContext";
|
|
2
|
+
|
|
3
|
+
describe("createOTelContext", () => {
|
|
4
|
+
it("should return an object with all expected methods", () => {
|
|
5
|
+
const ctx = createOTelContext();
|
|
6
|
+
expect(typeof ctx.OTelTracer).toBe("function");
|
|
7
|
+
expect(typeof ctx.OTelSetTracer).toBe("function");
|
|
8
|
+
expect(typeof ctx.OTelMeter).toBe("function");
|
|
9
|
+
expect(typeof ctx.OTelSetMeter).toBe("function");
|
|
10
|
+
expect(typeof ctx.OTelLogger).toBe("function");
|
|
11
|
+
expect(typeof ctx.OTelRequestSpan).toBe("function");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should create a StandardLogger lazily", () => {
|
|
15
|
+
const ctx = createOTelContext();
|
|
16
|
+
const logger = ctx.OTelLogger();
|
|
17
|
+
expect(logger).toBeDefined();
|
|
18
|
+
// Same instance on second call
|
|
19
|
+
expect(ctx.OTelLogger()).toBe(logger);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should store and retrieve tracer via setter/getter", () => {
|
|
23
|
+
const ctx = createOTelContext();
|
|
24
|
+
const fakeTracer = { id: "tracer-1" } as never;
|
|
25
|
+
ctx.OTelSetTracer(fakeTracer);
|
|
26
|
+
expect(ctx.OTelTracer()).toBe(fakeTracer);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should store and retrieve meter via setter/getter", () => {
|
|
30
|
+
const ctx = createOTelContext();
|
|
31
|
+
const fakeMeter = { id: "meter-1" } as never;
|
|
32
|
+
ctx.OTelSetMeter(fakeMeter);
|
|
33
|
+
expect(ctx.OTelMeter()).toBe(fakeMeter);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should isolate contexts from each other", () => {
|
|
37
|
+
const ctx1 = createOTelContext();
|
|
38
|
+
const ctx2 = createOTelContext();
|
|
39
|
+
const fakeTracer1 = { id: "tracer-1" } as never;
|
|
40
|
+
const fakeTracer2 = { id: "tracer-2" } as never;
|
|
41
|
+
ctx1.OTelSetTracer(fakeTracer1);
|
|
42
|
+
ctx2.OTelSetTracer(fakeTracer2);
|
|
43
|
+
expect(ctx1.OTelTracer()).toBe(fakeTracer1);
|
|
44
|
+
expect(ctx2.OTelTracer()).toBe(fakeTracer2);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("OTelRequestSpan should return undefined for object without tracerSpanApi", () => {
|
|
48
|
+
const ctx = createOTelContext();
|
|
49
|
+
expect(ctx.OTelRequestSpan({})).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("OTelRequestSpan should return the span when present", () => {
|
|
53
|
+
const ctx = createOTelContext();
|
|
54
|
+
const fakeSpan = { spanId: "123" } as never;
|
|
55
|
+
const req = { tracerSpanApi: fakeSpan };
|
|
56
|
+
expect(ctx.OTelRequestSpan(req)).toBe(fakeSpan);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StandardLogger,
|
|
3
|
+
StandardMeter,
|
|
4
|
+
StandardTracer,
|
|
5
|
+
} from "@devopsplaybook.io/otel-utils";
|
|
6
|
+
import { Span } from "@opentelemetry/sdk-trace-base";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Result of {@link createOTelContext}.
|
|
10
|
+
* Holds module-level singletons for the OpenTelemetry tracer, meter and logger
|
|
11
|
+
* used throughout a server process.
|
|
12
|
+
*/
|
|
13
|
+
export interface OTelContext {
|
|
14
|
+
OTelTracer: () => StandardTracer;
|
|
15
|
+
OTelSetTracer: (tracer: StandardTracer) => void;
|
|
16
|
+
OTelMeter: () => StandardMeter;
|
|
17
|
+
OTelSetMeter: (meter: StandardMeter) => void;
|
|
18
|
+
OTelLogger: () => StandardLogger;
|
|
19
|
+
/**
|
|
20
|
+
* Retrieves the span previously attached to a request object.
|
|
21
|
+
* Equivalent to `req.tracerSpanApi`.
|
|
22
|
+
*/
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
OTelRequestSpan: (req: any) => Span | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates an isolated OTel context (tracer / meter / logger singletons).
|
|
29
|
+
*
|
|
30
|
+
* Each server process should call this once at startup and pass the returned
|
|
31
|
+
* object to modules that need telemetry access. This avoids polluting the
|
|
32
|
+
* global module scope and makes unit-testing straightforward.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* const otel = createOTelContext();
|
|
37
|
+
* otel.OTelSetTracer(new StandardTracer(config));
|
|
38
|
+
* otel.OTelSetMeter(new StandardMeter(config));
|
|
39
|
+
* otel.OTelLogger().initOTel(config);
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function createOTelContext(): OTelContext {
|
|
43
|
+
let tracer: StandardTracer;
|
|
44
|
+
let meter: StandardMeter;
|
|
45
|
+
let logger: StandardLogger;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
OTelTracer: () => tracer,
|
|
49
|
+
OTelSetTracer: (t: StandardTracer) => {
|
|
50
|
+
tracer = t;
|
|
51
|
+
},
|
|
52
|
+
OTelMeter: () => meter,
|
|
53
|
+
OTelSetMeter: (m: StandardMeter) => {
|
|
54
|
+
meter = m;
|
|
55
|
+
},
|
|
56
|
+
OTelLogger: () => {
|
|
57
|
+
if (!logger) {
|
|
58
|
+
logger = new StandardLogger();
|
|
59
|
+
}
|
|
60
|
+
return logger;
|
|
61
|
+
},
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
|
+
OTelRequestSpan: (req: any) => req?.tracerSpanApi as Span | undefined,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
|
|
3
|
+
// Mock pg before imports
|
|
4
|
+
const mockQuery = jest.fn();
|
|
5
|
+
jest.mock("pg", () => {
|
|
6
|
+
return {
|
|
7
|
+
Pool: jest.fn().mockImplementation(() => ({
|
|
8
|
+
query: mockQuery,
|
|
9
|
+
on: jest.fn(),
|
|
10
|
+
})),
|
|
11
|
+
};
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
jest.mock("fs-extra", () => ({
|
|
15
|
+
readdir: jest.fn().mockResolvedValue(["init-0000.sql"]),
|
|
16
|
+
readFile: jest
|
|
17
|
+
.fn()
|
|
18
|
+
.mockResolvedValue(Buffer.from("CREATE TABLE metadata ();")),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
jest.mock("@devopsplaybook.io/otel-utils", () => ({
|
|
22
|
+
StandardTracer: jest.fn(),
|
|
23
|
+
StandardLogger: jest.fn(),
|
|
24
|
+
ModuleLogger: jest.fn(),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
jest.mock("@opentelemetry/sdk-trace-base", () => ({
|
|
28
|
+
Span: jest.fn(),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.mock("@opentelemetry/api", () => ({
|
|
32
|
+
SpanStatusCode: { ERROR: 2, OK: 1 },
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
describe("PostgresDbUtils", () => {
|
|
36
|
+
let PostgresDbUtils: typeof import("./PostgresDbUtils");
|
|
37
|
+
|
|
38
|
+
const mockSpan = {
|
|
39
|
+
end: jest.fn(),
|
|
40
|
+
addEvent: jest.fn(),
|
|
41
|
+
setStatus: jest.fn(),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const mockModuleLogger = {
|
|
45
|
+
info: jest.fn(),
|
|
46
|
+
error: jest.fn(),
|
|
47
|
+
warn: jest.fn(),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const mockTracer = {
|
|
51
|
+
startSpan: jest.fn().mockReturnValue(mockSpan),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const mockLogger = {
|
|
55
|
+
createModuleLogger: jest.fn().mockReturnValue(mockModuleLogger),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
jest.clearAllMocks();
|
|
60
|
+
jest.isolateModules(() => {
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
62
|
+
PostgresDbUtils = require("./PostgresDbUtils");
|
|
63
|
+
});
|
|
64
|
+
PostgresDbUtils.PostgresDbUtilsSetOTel(
|
|
65
|
+
mockTracer as any,
|
|
66
|
+
mockLogger as any,
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("PostgresDbUtilsSetOTel should accept tracer and logger", () => {
|
|
71
|
+
expect(() =>
|
|
72
|
+
PostgresDbUtils.PostgresDbUtilsSetOTel(
|
|
73
|
+
mockTracer as any,
|
|
74
|
+
mockLogger as any,
|
|
75
|
+
),
|
|
76
|
+
).not.toThrow();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("PostgresDbUtilsGetPool should return undefined before init", () => {
|
|
80
|
+
expect(PostgresDbUtils.PostgresDbUtilsGetPool()).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("PostgresDbUtilsExecSQL should create a span and call pool.query", async () => {
|
|
84
|
+
const config = {
|
|
85
|
+
DATABASE_POSTGRES_HOST: "localhost",
|
|
86
|
+
DATABASE_POSTGRES_PORT: 5432,
|
|
87
|
+
DATABASE_POSTGRES_USER: "user",
|
|
88
|
+
DATABASE_POSTGRES_PASSWORD: "pass",
|
|
89
|
+
DATABASE_POSTGRES_DATABASE: "testdb",
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Mock the version query to return version 0
|
|
93
|
+
// pool.query can be called with 2 args (sql, cb) or 3 args (sql, params, cb)
|
|
94
|
+
mockQuery.mockImplementation((...args: any[]) => {
|
|
95
|
+
const cb =
|
|
96
|
+
typeof args[args.length - 1] === "function"
|
|
97
|
+
? args[args.length - 1]
|
|
98
|
+
: null;
|
|
99
|
+
if (!cb) return;
|
|
100
|
+
// Determine which call this is based on arg count
|
|
101
|
+
if (args.length === 2) {
|
|
102
|
+
// execSQLFile call
|
|
103
|
+
cb(null);
|
|
104
|
+
} else if (args.length === 3) {
|
|
105
|
+
const sql = args[0] as string;
|
|
106
|
+
if (sql.includes("MAX")) {
|
|
107
|
+
// version query
|
|
108
|
+
cb(null, { rows: [{ version: 0 }] });
|
|
109
|
+
} else {
|
|
110
|
+
// insert version
|
|
111
|
+
cb(null, { rowCount: 1 });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await PostgresDbUtils.PostgresDbUtilsInit(
|
|
117
|
+
mockSpan as any,
|
|
118
|
+
config,
|
|
119
|
+
"/fake/sql",
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(mockTracer.startSpan).toHaveBeenCalledWith(
|
|
123
|
+
"PostgresDbUtilsInit",
|
|
124
|
+
mockSpan,
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("PostgresDbUtilsTransactionStart should call BEGIN", async () => {
|
|
129
|
+
const config = {
|
|
130
|
+
DATABASE_POSTGRES_HOST: "localhost",
|
|
131
|
+
DATABASE_POSTGRES_PORT: 5432,
|
|
132
|
+
DATABASE_POSTGRES_USER: "user",
|
|
133
|
+
DATABASE_POSTGRES_PASSWORD: "pass",
|
|
134
|
+
DATABASE_POSTGRES_DATABASE: "testdb",
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Need to init first to create pool
|
|
138
|
+
mockQuery.mockImplementation((_sql: any, ...args: any[]) => {
|
|
139
|
+
const cb = args[args.length - 1];
|
|
140
|
+
if (typeof cb === "function") cb(null, { rows: [], rowCount: 0 });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await PostgresDbUtils.PostgresDbUtilsInit(
|
|
144
|
+
mockSpan as any,
|
|
145
|
+
config,
|
|
146
|
+
"/fake/sql",
|
|
147
|
+
);
|
|
148
|
+
await PostgresDbUtils.PostgresDbUtilsTransactionStart(mockSpan as any);
|
|
149
|
+
|
|
150
|
+
expect(mockTracer.startSpan).toHaveBeenCalledWith(
|
|
151
|
+
"PostgresDbUtilsTransactionStart",
|
|
152
|
+
mockSpan,
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
});
|