@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.
- package/README.md +442 -0
- package/dist/ai/groqInsightService.d.ts +39 -0
- package/dist/ai/groqInsightService.js +230 -0
- package/dist/createFlowwatch.d.ts +17 -0
- package/dist/createFlowwatch.js +90 -0
- package/dist/dashboard/routes/dashboardResponse.d.ts +204 -0
- package/dist/dashboard/routes/dashboardResponse.js +248 -0
- package/dist/dashboard/routes/router.d.ts +13 -0
- package/dist/dashboard/routes/router.js +708 -0
- package/dist/dashboard/static/dashboard.html +6061 -0
- package/dist/engine/background/queues/workflowQueue.d.ts +6 -0
- package/dist/engine/background/queues/workflowQueue.js +14 -0
- package/dist/engine/background/workers/workflowWorker.d.ts +15 -0
- package/dist/engine/background/workers/workflowWorker.js +98 -0
- package/dist/engine/errors/errorEngine.d.ts +27 -0
- package/dist/engine/errors/errorEngine.js +115 -0
- package/dist/engine/flags/evaluateFlag.d.ts +3 -0
- package/dist/engine/flags/evaluateFlag.js +50 -0
- package/dist/engine/flags/flagEngine.d.ts +9 -0
- package/dist/engine/flags/flagEngine.js +52 -0
- package/dist/engine/flags/hashRollout.d.ts +1 -0
- package/dist/engine/flags/hashRollout.js +9 -0
- package/dist/engine/flags/types.d.ts +7 -0
- package/dist/engine/flags/types.js +1 -0
- package/dist/engine/trace/traceEngine.d.ts +26 -0
- package/dist/engine/trace/traceEngine.js +76 -0
- package/dist/engine/workflows/types.d.ts +28 -0
- package/dist/engine/workflows/types.js +1 -0
- package/dist/engine/workflows/workflowEngine.d.ts +15 -0
- package/dist/engine/workflows/workflowEngine.js +112 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +3 -0
- package/dist/persistence/cache/redisClient.d.ts +2 -0
- package/dist/persistence/cache/redisClient.js +4 -0
- package/dist/persistence/db/postgres.d.ts +3 -0
- package/dist/persistence/db/postgres.js +4 -0
- package/dist/persistence/migrations/migrationRunner.d.ts +3 -0
- package/dist/persistence/migrations/migrationRunner.js +46 -0
- package/dist/persistence/migrations/migrations.d.ts +5 -0
- package/dist/persistence/migrations/migrations.js +191 -0
- package/dist/persistence/repositories/errors/errorRepository.d.ts +38 -0
- package/dist/persistence/repositories/errors/errorRepository.js +63 -0
- package/dist/persistence/repositories/flags/flagRepository.d.ts +72 -0
- package/dist/persistence/repositories/flags/flagRepository.js +245 -0
- package/dist/persistence/repositories/traces/traceRepository.d.ts +64 -0
- package/dist/persistence/repositories/traces/traceRepository.js +110 -0
- package/dist/persistence/repositories/workflows/workflowRepository.d.ts +93 -0
- package/dist/persistence/repositories/workflows/workflowRepository.js +260 -0
- package/dist/persistence/transaction.d.ts +2 -0
- package/dist/persistence/transaction.js +16 -0
- package/dist/runtime/config/normalizeConfig.d.ts +2 -0
- package/dist/runtime/config/normalizeConfig.js +46 -0
- package/dist/runtime/config/validationConfig.d.ts +2 -0
- package/dist/runtime/config/validationConfig.js +119 -0
- package/dist/runtime/health/healthService.d.ts +30 -0
- package/dist/runtime/health/healthService.js +54 -0
- package/dist/runtime/tracing/traceContext.d.ts +12 -0
- package/dist/runtime/tracing/traceContext.js +28 -0
- package/dist/runtime/tracing/tracingMiddleware.d.ts +3 -0
- package/dist/runtime/tracing/tracingMiddleware.js +46 -0
- package/dist/search/elasticsearch/client.d.ts +2 -0
- package/dist/search/elasticsearch/client.js +4 -0
- package/dist/search/elasticsearch/indexSetup.d.ts +3 -0
- package/dist/search/elasticsearch/indexSetup.js +43 -0
- package/dist/search/elasticsearch/indexer.d.ts +9 -0
- package/dist/search/elasticsearch/indexer.js +86 -0
- package/dist/search/elasticsearch/mappingChecker.d.ts +2 -0
- package/dist/search/elasticsearch/mappingChecker.js +28 -0
- package/dist/types/index.d.ts +48 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/flowwatchEnvStore.d.ts +27 -0
- package/dist/utils/flowwatchEnvStore.js +145 -0
- 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,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,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,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,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
|
+
}
|