@pranshulsoni/flowwatch 1.0.0

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.
Files changed (73) hide show
  1. package/README.md +442 -0
  2. package/dist/ai/groqInsightService.d.ts +39 -0
  3. package/dist/ai/groqInsightService.js +230 -0
  4. package/dist/createFlowwatch.d.ts +17 -0
  5. package/dist/createFlowwatch.js +90 -0
  6. package/dist/dashboard/routes/dashboardResponse.d.ts +204 -0
  7. package/dist/dashboard/routes/dashboardResponse.js +248 -0
  8. package/dist/dashboard/routes/router.d.ts +13 -0
  9. package/dist/dashboard/routes/router.js +708 -0
  10. package/dist/dashboard/static/dashboard.html +6061 -0
  11. package/dist/engine/background/queues/workflowQueue.d.ts +6 -0
  12. package/dist/engine/background/queues/workflowQueue.js +14 -0
  13. package/dist/engine/background/workers/workflowWorker.d.ts +15 -0
  14. package/dist/engine/background/workers/workflowWorker.js +98 -0
  15. package/dist/engine/errors/errorEngine.d.ts +27 -0
  16. package/dist/engine/errors/errorEngine.js +115 -0
  17. package/dist/engine/flags/evaluateFlag.d.ts +3 -0
  18. package/dist/engine/flags/evaluateFlag.js +50 -0
  19. package/dist/engine/flags/flagEngine.d.ts +9 -0
  20. package/dist/engine/flags/flagEngine.js +52 -0
  21. package/dist/engine/flags/hashRollout.d.ts +1 -0
  22. package/dist/engine/flags/hashRollout.js +9 -0
  23. package/dist/engine/flags/types.d.ts +7 -0
  24. package/dist/engine/flags/types.js +1 -0
  25. package/dist/engine/trace/traceEngine.d.ts +26 -0
  26. package/dist/engine/trace/traceEngine.js +76 -0
  27. package/dist/engine/workflows/types.d.ts +28 -0
  28. package/dist/engine/workflows/types.js +1 -0
  29. package/dist/engine/workflows/workflowEngine.d.ts +15 -0
  30. package/dist/engine/workflows/workflowEngine.js +112 -0
  31. package/dist/index.d.ts +9 -0
  32. package/dist/index.js +3 -0
  33. package/dist/persistence/cache/redisClient.d.ts +2 -0
  34. package/dist/persistence/cache/redisClient.js +4 -0
  35. package/dist/persistence/db/postgres.d.ts +3 -0
  36. package/dist/persistence/db/postgres.js +4 -0
  37. package/dist/persistence/migrations/migrationRunner.d.ts +3 -0
  38. package/dist/persistence/migrations/migrationRunner.js +46 -0
  39. package/dist/persistence/migrations/migrations.d.ts +5 -0
  40. package/dist/persistence/migrations/migrations.js +191 -0
  41. package/dist/persistence/repositories/errors/errorRepository.d.ts +38 -0
  42. package/dist/persistence/repositories/errors/errorRepository.js +63 -0
  43. package/dist/persistence/repositories/flags/flagRepository.d.ts +72 -0
  44. package/dist/persistence/repositories/flags/flagRepository.js +245 -0
  45. package/dist/persistence/repositories/traces/traceRepository.d.ts +64 -0
  46. package/dist/persistence/repositories/traces/traceRepository.js +110 -0
  47. package/dist/persistence/repositories/workflows/workflowRepository.d.ts +93 -0
  48. package/dist/persistence/repositories/workflows/workflowRepository.js +260 -0
  49. package/dist/persistence/transaction.d.ts +2 -0
  50. package/dist/persistence/transaction.js +16 -0
  51. package/dist/runtime/config/normalizeConfig.d.ts +2 -0
  52. package/dist/runtime/config/normalizeConfig.js +46 -0
  53. package/dist/runtime/config/validationConfig.d.ts +2 -0
  54. package/dist/runtime/config/validationConfig.js +119 -0
  55. package/dist/runtime/health/healthService.d.ts +30 -0
  56. package/dist/runtime/health/healthService.js +54 -0
  57. package/dist/runtime/tracing/traceContext.d.ts +12 -0
  58. package/dist/runtime/tracing/traceContext.js +28 -0
  59. package/dist/runtime/tracing/tracingMiddleware.d.ts +3 -0
  60. package/dist/runtime/tracing/tracingMiddleware.js +46 -0
  61. package/dist/search/elasticsearch/client.d.ts +2 -0
  62. package/dist/search/elasticsearch/client.js +4 -0
  63. package/dist/search/elasticsearch/indexSetup.d.ts +3 -0
  64. package/dist/search/elasticsearch/indexSetup.js +43 -0
  65. package/dist/search/elasticsearch/indexer.d.ts +9 -0
  66. package/dist/search/elasticsearch/indexer.js +86 -0
  67. package/dist/search/elasticsearch/mappingChecker.d.ts +2 -0
  68. package/dist/search/elasticsearch/mappingChecker.js +28 -0
  69. package/dist/types/index.d.ts +48 -0
  70. package/dist/types/index.js +1 -0
  71. package/dist/utils/flowwatchEnvStore.d.ts +27 -0
  72. package/dist/utils/flowwatchEnvStore.js +145 -0
  73. package/package.json +63 -0
@@ -0,0 +1,260 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { withTransaction } from "../../transaction.js";
3
+ export async function insertWorkflow(pool, input) {
4
+ const workflowId = randomUUID();
5
+ const version = input.version ?? 1;
6
+ return withTransaction(pool, async (client) => {
7
+ const workflowResult = await client.query(`
8
+ INSERT INTO flowwatch_workflows (id, name, version)
9
+ VALUES ($1, $2, $3)
10
+ ON CONFLICT (name, version)
11
+ DO UPDATE SET updated_at = now()
12
+ RETURNING id, name, version
13
+ `, [workflowId, input.name, version]);
14
+ const workflow = workflowResult.rows[0];
15
+ const workflowSteps = [];
16
+ for (let i = 0; i < input.steps.length; i++) {
17
+ const step = input.steps[i];
18
+ const stepResult = await client.query(`
19
+ INSERT INTO flowwatch_workflow_steps (
20
+ id,
21
+ workflow_id,
22
+ step_index,
23
+ name,
24
+ max_retries
25
+ )
26
+ VALUES ($1, $2, $3, $4, $5)
27
+ ON CONFLICT (workflow_id, step_index)
28
+ DO UPDATE SET
29
+ name = EXCLUDED.name,
30
+ max_retries = EXCLUDED.max_retries
31
+ RETURNING id, workflow_id, step_index, name, max_retries
32
+ `, [
33
+ randomUUID(),
34
+ workflow.id,
35
+ i,
36
+ step.name,
37
+ step.maxRetries ?? 0,
38
+ ]);
39
+ const savedStep = stepResult.rows[0];
40
+ workflowSteps.push({
41
+ id: savedStep.id,
42
+ workflowId: savedStep.workflow_id,
43
+ stepIndex: savedStep.step_index,
44
+ name: savedStep.name,
45
+ maxRetries: savedStep.max_retries,
46
+ });
47
+ }
48
+ return {
49
+ workflow,
50
+ steps: workflowSteps,
51
+ };
52
+ });
53
+ }
54
+ export async function insertWorkflowExecution(pool, input) {
55
+ const executionId = randomUUID();
56
+ return withTransaction(pool, async (client) => {
57
+ await client.query(`
58
+ INSERT INTO flowwatch_workflow_executions (
59
+ id,
60
+ workflow_id,
61
+ workflow_name,
62
+ workflow_version,
63
+ status,
64
+ input
65
+ )
66
+ VALUES ($1, $2, $3, $4, $5, $6)
67
+ `, [
68
+ executionId,
69
+ input.workflowId,
70
+ input.workflowName,
71
+ input.workflowVersion,
72
+ "queued",
73
+ JSON.stringify(input.input)
74
+ ]);
75
+ for (const step of input.steps) {
76
+ await client.query(`
77
+ INSERT INTO flowwatch_workflow_step_executions (
78
+ id,
79
+ execution_id,
80
+ workflow_step_id,
81
+ step_index,
82
+ step_name,
83
+ status,
84
+ input,
85
+ attempt_count,
86
+ max_retries
87
+ )
88
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
89
+ `, [
90
+ randomUUID(),
91
+ executionId,
92
+ step.workflowStepId,
93
+ step.stepIndex,
94
+ step.stepName,
95
+ "pending",
96
+ JSON.stringify(step.input),
97
+ 0,
98
+ step.maxRetries
99
+ ]);
100
+ }
101
+ return { executionId };
102
+ });
103
+ }
104
+ export async function getWorkflowExecution(pool, executionId) {
105
+ const result = await pool.query(`SELECT * FROM flowwatch_workflow_executions WHERE id = $1`, [executionId]);
106
+ return result.rows[0];
107
+ }
108
+ export async function listWorkflowDefinitions(pool) {
109
+ const result = await pool.query(`
110
+ SELECT *
111
+ FROM flowwatch_workflows
112
+ ORDER BY name ASC, version DESC
113
+ `);
114
+ return result.rows;
115
+ }
116
+ export async function getLatestWorkflowDefinitionByName(pool, workflowName) {
117
+ const result = await pool.query(`
118
+ SELECT *
119
+ FROM flowwatch_workflows
120
+ WHERE name = $1
121
+ ORDER BY version DESC
122
+ LIMIT 1
123
+ `, [workflowName]);
124
+ return result.rows[0];
125
+ }
126
+ export async function listWorkflowExecutions(pool, limit = 50) {
127
+ const result = await pool.query(`
128
+ SELECT *
129
+ FROM flowwatch_workflow_executions
130
+ ORDER BY created_at DESC
131
+ LIMIT $1
132
+ `, [limit]);
133
+ return result.rows;
134
+ }
135
+ export async function listWorkflowExecutionsByWorkflowName(pool, workflowName, limit = 50) {
136
+ const result = await pool.query(`
137
+ SELECT *
138
+ FROM flowwatch_workflow_executions
139
+ WHERE workflow_name = $1
140
+ ORDER BY created_at DESC
141
+ LIMIT $2
142
+ `, [workflowName, limit]);
143
+ return result.rows;
144
+ }
145
+ export async function listWorkflowStepExecutionsByExecutionIds(pool, executionIds) {
146
+ if (executionIds.length === 0) {
147
+ return new Map();
148
+ }
149
+ const result = await pool.query(`
150
+ SELECT *
151
+ FROM flowwatch_workflow_step_executions
152
+ WHERE execution_id = ANY($1::uuid[])
153
+ ORDER BY execution_id ASC, step_index ASC
154
+ `, [executionIds]);
155
+ const grouped = new Map();
156
+ for (const row of result.rows) {
157
+ const existing = grouped.get(row.execution_id) || [];
158
+ existing.push(row);
159
+ grouped.set(row.execution_id, existing);
160
+ }
161
+ return grouped;
162
+ }
163
+ export async function listWorkflowStepsByWorkflowIds(pool, workflowIds) {
164
+ if (workflowIds.length === 0) {
165
+ return new Map();
166
+ }
167
+ const result = await pool.query(`
168
+ SELECT *
169
+ FROM flowwatch_workflow_steps
170
+ WHERE workflow_id = ANY($1::uuid[])
171
+ ORDER BY workflow_id ASC, step_index ASC
172
+ `, [workflowIds]);
173
+ const grouped = new Map();
174
+ for (const row of result.rows) {
175
+ const existing = grouped.get(row.workflow_id) || [];
176
+ existing.push({
177
+ id: row.id,
178
+ workflowId: row.workflow_id,
179
+ stepIndex: row.step_index,
180
+ name: row.name,
181
+ maxRetries: row.max_retries,
182
+ });
183
+ grouped.set(row.workflow_id, existing);
184
+ }
185
+ return grouped;
186
+ }
187
+ export async function getWorkflowExecutionSteps(pool, executionId) {
188
+ const result = await pool.query(`
189
+ SELECT *
190
+ FROM flowwatch_workflow_step_executions
191
+ WHERE execution_id = $1
192
+ ORDER BY step_index ASC
193
+ `, [executionId]);
194
+ return result.rows;
195
+ }
196
+ export async function markWorkflowExecutionRunning(pool, executionId) {
197
+ await pool.query(`
198
+ UPDATE flowwatch_workflow_executions
199
+ SET status = 'running',
200
+ started_at = COALESCE(started_at, now())
201
+ WHERE id = $1
202
+ `, [executionId]);
203
+ }
204
+ export async function markWorkflowExecutionCompleted(pool, executionId, output) {
205
+ await pool.query(`
206
+ UPDATE flowwatch_workflow_executions
207
+ SET status = 'completed',
208
+ output = $2,
209
+ completed_at = now()
210
+ WHERE id = $1
211
+ `, [executionId, JSON.stringify(output)]);
212
+ }
213
+ export async function markWorkflowExecutionFailed(pool, executionId, error) {
214
+ await pool.query(`
215
+ UPDATE flowwatch_workflow_executions
216
+ SET status = 'failed',
217
+ error = $2,
218
+ failed_at = now()
219
+ WHERE id = $1
220
+ `, [executionId, JSON.stringify(serializeError(error))]);
221
+ }
222
+ export async function markWorkflowStepRunning(pool, stepExecutionId) {
223
+ await pool.query(`
224
+ UPDATE flowwatch_workflow_step_executions
225
+ SET status = 'running',
226
+ started_at = COALESCE(started_at, now())
227
+ WHERE id = $1
228
+ `, [stepExecutionId]);
229
+ }
230
+ export async function markWorkflowStepCompleted(pool, stepExecutionId, output) {
231
+ await pool.query(`
232
+ UPDATE flowwatch_workflow_step_executions
233
+ SET status = 'completed',
234
+ output = $2,
235
+ completed_at = now()
236
+ WHERE id = $1
237
+ `, [stepExecutionId, JSON.stringify(output)]);
238
+ }
239
+ export async function markWorkflowStepFailed(pool, stepExecutionId, error) {
240
+ await pool.query(`
241
+ UPDATE flowwatch_workflow_step_executions
242
+ SET status = 'failed',
243
+ error = $2,
244
+ failed_at = now(),
245
+ attempt_count = attempt_count + 1
246
+ WHERE id = $1
247
+ `, [stepExecutionId, JSON.stringify(serializeError(error))]);
248
+ }
249
+ function serializeError(error) {
250
+ if (error instanceof Error) {
251
+ return {
252
+ name: error.name,
253
+ message: error.message,
254
+ stack: error.stack,
255
+ };
256
+ }
257
+ return {
258
+ message: String(error),
259
+ };
260
+ }
@@ -0,0 +1,2 @@
1
+ import type { Pool, PoolClient } from "pg";
2
+ export declare function withTransaction<T>(pool: Pool, fn: (client: PoolClient) => Promise<T>): Promise<T>;
@@ -0,0 +1,16 @@
1
+ export async function withTransaction(pool, fn) {
2
+ const client = await pool.connect();
3
+ try {
4
+ await client.query("BEGIN");
5
+ const result = await fn(client);
6
+ await client.query("COMMIT");
7
+ return result;
8
+ }
9
+ catch (error) {
10
+ await client.query("ROLLBACK");
11
+ throw error;
12
+ }
13
+ finally {
14
+ client.release();
15
+ }
16
+ }
@@ -0,0 +1,2 @@
1
+ import type { NormalizedFlowwatchConfig, FlowwatchConfig } from "../../types/index.js";
2
+ export declare function normalizeConfig(config: FlowwatchConfig): Promise<NormalizedFlowwatchConfig>;
@@ -0,0 +1,46 @@
1
+ export async function normalizeConfig(config) {
2
+ const workerConfig = normalizeWorkerConfig(config.worker);
3
+ return {
4
+ db: config.db,
5
+ redis: {
6
+ url: config.redis.url,
7
+ },
8
+ elasticsearch: {
9
+ node: config.elasticsearch.node,
10
+ },
11
+ dashboard: {
12
+ path: config.dashboard?.path ?? "/flowwatch",
13
+ enabled: config.dashboard?.enabled ?? true,
14
+ token: config.dashboard?.token,
15
+ auth: config.dashboard?.auth,
16
+ },
17
+ worker: workerConfig,
18
+ migrations: {
19
+ autoRun: config.migrations?.autoRun ?? false,
20
+ tableName: config.migrations?.tableName ?? "flowwatch_migrations",
21
+ },
22
+ runtime: {
23
+ environment: config.runtime?.environment ?? process.env.NODE_ENV ?? "development",
24
+ serviceName: config.runtime?.serviceName ?? "flowwatch",
25
+ debug: config.runtime?.debug ?? false,
26
+ },
27
+ };
28
+ }
29
+ function normalizeWorkerConfig(worker) {
30
+ if (typeof worker === "boolean") {
31
+ return {
32
+ enabled: worker,
33
+ workflowConcurrency: 5,
34
+ errorIndexingConcurrency: 2,
35
+ maintenanceConcurrency: 1,
36
+ queuePrefix: "flowwatch",
37
+ };
38
+ }
39
+ return {
40
+ enabled: worker?.enabled ?? true,
41
+ workflowConcurrency: worker?.workflowConcurrency ?? 5,
42
+ errorIndexingConcurrency: worker?.errorIndexingConcurrency ?? 2,
43
+ maintenanceConcurrency: worker?.maintenanceConcurrency ?? 1,
44
+ queuePrefix: worker?.queuePrefix ?? "flowwatch",
45
+ };
46
+ }
@@ -0,0 +1,2 @@
1
+ import type { FlowwatchConfig } from "../../types/index.js";
2
+ export declare function validateConfig(config: unknown): FlowwatchConfig;
@@ -0,0 +1,119 @@
1
+ export function validateConfig(config) {
2
+ if (!isObject(config)) {
3
+ throw new Error("config must be an object");
4
+ }
5
+ validateDbConfig(config.db);
6
+ validateRedisConfig(config.redis);
7
+ validateElasticsearchConfig(config.elasticsearch);
8
+ validateDashboardConfig(config.dashboard);
9
+ validateWorkerConfig(config.worker);
10
+ validateMigrationsConfig(config.migrations);
11
+ validateRuntimeConfig(config.runtime);
12
+ return config;
13
+ }
14
+ function validateDbConfig(db) {
15
+ if (!isObject(db)) {
16
+ throw new Error("config db must be an object");
17
+ }
18
+ }
19
+ function validateRedisConfig(redis) {
20
+ if (!isObject(redis)) {
21
+ throw new Error("config redis must be an object");
22
+ }
23
+ if (!isNonEmptyString(redis.url)) {
24
+ throw new Error("config redis.url must be a non-empty string");
25
+ }
26
+ }
27
+ function validateElasticsearchConfig(elasticsearch) {
28
+ if (!isObject(elasticsearch)) {
29
+ throw new Error("config elasticsearch must be an object");
30
+ }
31
+ if (!isNonEmptyString(elasticsearch.node)) {
32
+ throw new Error("config elasticsearch.node must be a non-empty string");
33
+ }
34
+ }
35
+ function validateDashboardConfig(dashboard) {
36
+ if (dashboard === undefined) {
37
+ return;
38
+ }
39
+ if (!isObject(dashboard)) {
40
+ throw new Error("config dashboard must be an object");
41
+ }
42
+ if (dashboard.path !== undefined && (!isNonEmptyString(dashboard.path) || !dashboard.path.startsWith("/"))) {
43
+ throw new Error("config dashboard.path must be a non-empty string starting with /");
44
+ }
45
+ if (dashboard.token !== undefined && !isNonEmptyString(dashboard.token)) {
46
+ throw new Error("config dashboard.token must be a non-empty string");
47
+ }
48
+ if (dashboard.auth !== undefined && typeof dashboard.auth !== "function") {
49
+ throw new Error("config dashboard.auth must be a function");
50
+ }
51
+ if (dashboard.enabled !== undefined && typeof dashboard.enabled !== "boolean") {
52
+ throw new Error("config dashboard.enabled must be a boolean");
53
+ }
54
+ }
55
+ function validateWorkerConfig(worker) {
56
+ if (worker === undefined) {
57
+ return;
58
+ }
59
+ if (typeof worker === "boolean") {
60
+ return;
61
+ }
62
+ if (!isObject(worker)) {
63
+ throw new Error("config worker must be a boolean or an object");
64
+ }
65
+ if (worker.enabled !== undefined && typeof worker.enabled !== "boolean") {
66
+ throw new Error("config worker.enabled must be a boolean");
67
+ }
68
+ validatePositiveInteger(worker.workflowConcurrency, "worker.workflowConcurrency");
69
+ validatePositiveInteger(worker.errorIndexingConcurrency, "worker.errorIndexingConcurrency");
70
+ validatePositiveInteger(worker.maintenanceConcurrency, "worker.maintenanceConcurrency");
71
+ if (worker.queuePrefix !== undefined && !isNonEmptyString(worker.queuePrefix)) {
72
+ throw new Error("config worker.queuePrefix must be a non-empty string");
73
+ }
74
+ }
75
+ function validateMigrationsConfig(migrations) {
76
+ if (migrations === undefined) {
77
+ return;
78
+ }
79
+ if (!isObject(migrations)) {
80
+ throw new Error("config migrations must be an object");
81
+ }
82
+ if (migrations.autoRun !== undefined && typeof migrations.autoRun !== "boolean") {
83
+ throw new Error("config migrations.autoRun must be a boolean");
84
+ }
85
+ if (migrations.tableName !== undefined && !isNonEmptyString(migrations.tableName)) {
86
+ throw new Error("config migrations.tableName must be a non-empty string");
87
+ }
88
+ }
89
+ function validateRuntimeConfig(runtime) {
90
+ if (runtime === undefined) {
91
+ return;
92
+ }
93
+ if (!isObject(runtime)) {
94
+ throw new Error("config runtime must be an object");
95
+ }
96
+ if (runtime.environment !== undefined && !isNonEmptyString(runtime.environment)) {
97
+ throw new Error("config runtime.environment must be a non-empty string");
98
+ }
99
+ if (runtime.serviceName !== undefined && !isNonEmptyString(runtime.serviceName)) {
100
+ throw new Error("config runtime.serviceName must be a non-empty string");
101
+ }
102
+ if (runtime.debug !== undefined && typeof runtime.debug !== "boolean") {
103
+ throw new Error("config runtime.debug must be a boolean");
104
+ }
105
+ }
106
+ function validatePositiveInteger(value, fieldName) {
107
+ if (value === undefined) {
108
+ return;
109
+ }
110
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
111
+ throw new Error(`config ${fieldName} must be a positive integer`);
112
+ }
113
+ }
114
+ function isObject(value) {
115
+ return typeof value === "object" && value !== null && !Array.isArray(value);
116
+ }
117
+ function isNonEmptyString(value) {
118
+ return typeof value === "string" && value.trim().length > 0;
119
+ }
@@ -0,0 +1,30 @@
1
+ import type { Client } from "@elastic/elasticsearch";
2
+ import type { Pool } from "pg";
3
+ import type { Redis } from "ioredis";
4
+ export declare function checkPostgresHealth(pool: Pool): Promise<{
5
+ status: string;
6
+ latencyMs: number;
7
+ message?: undefined;
8
+ } | {
9
+ status: string;
10
+ message: string;
11
+ latencyMs?: undefined;
12
+ }>;
13
+ export declare function checkRedisHealth(redisClient: Redis): Promise<{
14
+ status: string;
15
+ message: string;
16
+ latencyMs?: undefined;
17
+ } | {
18
+ status: string;
19
+ latencyMs: number;
20
+ message?: undefined;
21
+ }>;
22
+ export declare function checkElasticsearchHealth(elasticsearchClient: Client): Promise<{
23
+ status: string;
24
+ latencyMs: number;
25
+ message?: undefined;
26
+ } | {
27
+ status: string;
28
+ message: string;
29
+ latencyMs?: undefined;
30
+ }>;
@@ -0,0 +1,54 @@
1
+ export async function checkPostgresHealth(pool) {
2
+ const startedAt = Date.now();
3
+ try {
4
+ await pool.query("SELECT 1");
5
+ return {
6
+ status: "ok",
7
+ latencyMs: Date.now() - startedAt
8
+ };
9
+ }
10
+ catch (error) {
11
+ return {
12
+ status: "error",
13
+ message: error instanceof Error ? error.message : "Unknown error"
14
+ };
15
+ }
16
+ }
17
+ export async function checkRedisHealth(redisClient) {
18
+ const startedAt = Date.now();
19
+ try {
20
+ const response = await redisClient.ping();
21
+ if (response != "PONG") {
22
+ return {
23
+ status: "error",
24
+ message: `Unexpected Redis Response ${response}`
25
+ };
26
+ }
27
+ return {
28
+ status: "ok",
29
+ latencyMs: Date.now() - startedAt
30
+ };
31
+ }
32
+ catch (error) {
33
+ return {
34
+ status: "error",
35
+ message: error instanceof Error ? error.message : "Unknown error"
36
+ };
37
+ }
38
+ }
39
+ export async function checkElasticsearchHealth(elasticsearchClient) {
40
+ const startedAt = Date.now();
41
+ try {
42
+ await elasticsearchClient.ping();
43
+ return {
44
+ status: "ok",
45
+ latencyMs: Date.now() - startedAt
46
+ };
47
+ }
48
+ catch (error) {
49
+ return {
50
+ status: "error",
51
+ message: error instanceof Error ? error.message : "Unknown error"
52
+ };
53
+ }
54
+ }
@@ -0,0 +1,12 @@
1
+ export interface TraceContext {
2
+ traceId: string;
3
+ currentSpanId?: string;
4
+ userId?: string;
5
+ ip?: string;
6
+ }
7
+ export declare function runWithTraceContext<T>(context: TraceContext, callback: () => T): T;
8
+ export declare function getCurrentTraceContext(): TraceContext | undefined;
9
+ export declare function getCurrentClientIp(): string | undefined;
10
+ export declare function getCurrentTraceId(): string | undefined;
11
+ export declare function getCurrentSpanId(): string | undefined;
12
+ export declare function runWithSpanContext<T>(spanId: string, callback: () => T): T;
@@ -0,0 +1,28 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const traceStorage = new AsyncLocalStorage();
3
+ //Added a generic class named async local storage with the type tracecontext, as its a generic class all its methods also return the generic type.
4
+ export function runWithTraceContext(context, callback) {
5
+ return traceStorage.run(context, callback);
6
+ }
7
+ export function getCurrentTraceContext() {
8
+ return traceStorage.getStore();
9
+ }
10
+ export function getCurrentClientIp() {
11
+ return traceStorage.getStore()?.ip;
12
+ }
13
+ export function getCurrentTraceId() {
14
+ return traceStorage.getStore()?.traceId;
15
+ }
16
+ export function getCurrentSpanId() {
17
+ return traceStorage.getStore()?.currentSpanId;
18
+ }
19
+ export function runWithSpanContext(spanId, callback) {
20
+ const currentContext = traceStorage.getStore();
21
+ if (!currentContext) {
22
+ return callback();
23
+ }
24
+ return traceStorage.run({
25
+ ...currentContext,
26
+ currentSpanId: spanId,
27
+ }, callback);
28
+ }
@@ -0,0 +1,3 @@
1
+ import type { RequestHandler } from "express";
2
+ import type { Pool } from "pg";
3
+ export declare function createRequestTracingMiddleware(pool: Pool): RequestHandler;
@@ -0,0 +1,46 @@
1
+ import { createRequestTrace, finishRequestTrace } from "../../persistence/repositories/traces/traceRepository.js";
2
+ import { runWithTraceContext } from "./traceContext.js";
3
+ export function createRequestTracingMiddleware(pool) {
4
+ return async function requestTracingMiddleware(req, res, next) {
5
+ const startedAt = Date.now();
6
+ const clientIp = getClientIp(req);
7
+ try {
8
+ const trace = await createRequestTrace(pool, {
9
+ method: req.method,
10
+ path: req.originalUrl || req.path,
11
+ ip: clientIp,
12
+ });
13
+ res.on("finish", () => {
14
+ finishRequestTrace(pool, {
15
+ traceId: trace.id,
16
+ statusCode: res.statusCode,
17
+ durationMs: Date.now() - startedAt,
18
+ }).catch(() => {
19
+ });
20
+ });
21
+ runWithTraceContext({ traceId: trace.id, ip: clientIp, }, next);
22
+ }
23
+ catch {
24
+ next();
25
+ }
26
+ };
27
+ }
28
+ function getClientIp(req) {
29
+ const forwardedFor = req.get("x-forwarded-for");
30
+ if (forwardedFor) {
31
+ return normalizeClientIp(forwardedFor.split(",")[0]?.trim());
32
+ }
33
+ return normalizeClientIp(req.ip || req.socket.remoteAddress || undefined);
34
+ }
35
+ function normalizeClientIp(value) {
36
+ if (!value)
37
+ return undefined;
38
+ const ip = value.trim();
39
+ if (ip === "::1" || ip === "::ffff:127.0.0.1") {
40
+ return "127.0.0.1";
41
+ }
42
+ if (ip.startsWith("::ffff:")) {
43
+ return ip.slice("::ffff:".length);
44
+ }
45
+ return ip;
46
+ }
@@ -0,0 +1,2 @@
1
+ import { Client } from "@elastic/elasticsearch";
2
+ export declare function createElasticsearchClient(node: string): Client;
@@ -0,0 +1,4 @@
1
+ import { Client } from "@elastic/elasticsearch";
2
+ export function createElasticsearchClient(node) {
3
+ return new Client({ node });
4
+ }
@@ -0,0 +1,3 @@
1
+ import type { Client } from "@elastic/elasticsearch";
2
+ export declare function createErrorMapping(client: Client): Promise<void>;
3
+ export declare function createTraceMapping(client: Client): Promise<void>;