@pagopa/opex-dashboard 0.0.1 → 0.1.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 +78 -1
- package/bin/index.js +1476 -0
- package/config.schema.json +174 -118
- package/package.json +18 -18
package/bin/index.js
ADDED
|
@@ -0,0 +1,1476 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// src/cli/index.ts
|
|
9
|
+
import { Command as Command2 } from "commander";
|
|
10
|
+
|
|
11
|
+
// src/cli/commands/generate.ts
|
|
12
|
+
import { Command } from "commander";
|
|
13
|
+
|
|
14
|
+
// src/utils/merge.ts
|
|
15
|
+
function overrideWith(source, overrides) {
|
|
16
|
+
const result = { ...source };
|
|
17
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
18
|
+
if (isPlainObject(value)) {
|
|
19
|
+
const sourceValue = result[key];
|
|
20
|
+
result[key] = overrideWith(
|
|
21
|
+
isPlainObject(sourceValue) ? sourceValue : {},
|
|
22
|
+
value
|
|
23
|
+
);
|
|
24
|
+
} else {
|
|
25
|
+
result[key] = value;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
function isPlainObject(value) {
|
|
31
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.prototype.toString.call(value) === "[object Object]";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/utils/normalize-endpoints.ts
|
|
35
|
+
function normalizeEndpointKeys(endpoints) {
|
|
36
|
+
const normalized = {};
|
|
37
|
+
for (const [key, value] of Object.entries(endpoints)) {
|
|
38
|
+
const spaceIndex = key.indexOf(" ");
|
|
39
|
+
if (spaceIndex > 0) {
|
|
40
|
+
const method = key.substring(0, spaceIndex);
|
|
41
|
+
const path4 = key.substring(spaceIndex + 1);
|
|
42
|
+
normalized[path4] = {
|
|
43
|
+
...value,
|
|
44
|
+
method
|
|
45
|
+
};
|
|
46
|
+
} else {
|
|
47
|
+
normalized[key] = value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return normalized;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/utils/parse-endpoint-key.ts
|
|
54
|
+
function parseEndpointKey(endpoint) {
|
|
55
|
+
const spaceIndex = endpoint.indexOf(" ");
|
|
56
|
+
const hasMethod = spaceIndex > 0;
|
|
57
|
+
if (hasMethod) {
|
|
58
|
+
return {
|
|
59
|
+
hasMethod: true,
|
|
60
|
+
method: endpoint.substring(0, spaceIndex),
|
|
61
|
+
path: endpoint.substring(spaceIndex + 1)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
hasMethod: false,
|
|
66
|
+
method: "",
|
|
67
|
+
path: endpoint
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/builders/base.ts
|
|
72
|
+
var Builder = class {
|
|
73
|
+
properties;
|
|
74
|
+
templateFn;
|
|
75
|
+
constructor(templateFn, baseProperties) {
|
|
76
|
+
this.templateFn = templateFn;
|
|
77
|
+
this.properties = baseProperties;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Package the output into a directory with additional assets.
|
|
81
|
+
* Default implementation throws error - override in subclasses that support packaging.
|
|
82
|
+
*/
|
|
83
|
+
package(outputPath, values = {}) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Packaging not supported for ${this.constructor.name}. Only azure-dashboard template type supports packaging.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Render the template by merging base properties and given values.
|
|
90
|
+
*/
|
|
91
|
+
produce(values = {}) {
|
|
92
|
+
const context = overrideWith(this.properties, values);
|
|
93
|
+
return this.templateFn(context);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get all base properties.
|
|
97
|
+
*/
|
|
98
|
+
props() {
|
|
99
|
+
return this.properties;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// src/builders/azure-dashboard-raw/builder.schema.ts
|
|
104
|
+
import { z as z2 } from "zod";
|
|
105
|
+
|
|
106
|
+
// src/constants/index.ts
|
|
107
|
+
var DEFAULT_AVAILABILITY_THRESHOLD = 0.99;
|
|
108
|
+
var DEFAULT_RESPONSE_TIME_THRESHOLD = 1;
|
|
109
|
+
var EVALUATION_FREQUENCY_MINUTES = 10;
|
|
110
|
+
var TIME_WINDOW_MINUTES = 20;
|
|
111
|
+
var EVENT_OCCURRENCES = 1;
|
|
112
|
+
|
|
113
|
+
// src/core/shared/endpoint-properties.schema.ts
|
|
114
|
+
import { z } from "zod";
|
|
115
|
+
var BaseEndpointEvaluationPropertiesSchema = z.object({
|
|
116
|
+
// Availability monitoring properties
|
|
117
|
+
availability_evaluation_frequency: z.number(),
|
|
118
|
+
availability_evaluation_time_window: z.number(),
|
|
119
|
+
availability_event_occurrences: z.number(),
|
|
120
|
+
availability_threshold: z.number(),
|
|
121
|
+
// Response time monitoring properties
|
|
122
|
+
response_time_evaluation_frequency: z.number(),
|
|
123
|
+
response_time_evaluation_time_window: z.number(),
|
|
124
|
+
response_time_event_occurrences: z.number(),
|
|
125
|
+
response_time_threshold: z.number()
|
|
126
|
+
});
|
|
127
|
+
var EndpointOverridePropertiesSchema = BaseEndpointEvaluationPropertiesSchema.partial().describe(
|
|
128
|
+
"Optional overrides for endpoint-specific alarm thresholds and evaluation settings"
|
|
129
|
+
);
|
|
130
|
+
var createEndpointConfigPropertiesSchema = (defaults) => z.object({
|
|
131
|
+
availability_evaluation_frequency: z.number().default(defaults.evaluationFrequency),
|
|
132
|
+
availability_evaluation_time_window: z.number().default(defaults.evaluationTimeWindow),
|
|
133
|
+
availability_event_occurrences: z.number().default(defaults.eventOccurrences),
|
|
134
|
+
availability_threshold: z.number().default(defaults.availabilityThreshold),
|
|
135
|
+
response_time_evaluation_frequency: z.number().default(defaults.evaluationFrequency),
|
|
136
|
+
response_time_evaluation_time_window: z.number().default(defaults.evaluationTimeWindow),
|
|
137
|
+
response_time_event_occurrences: z.number().default(defaults.eventOccurrences),
|
|
138
|
+
response_time_threshold: z.number().default(defaults.responseTimeThreshold)
|
|
139
|
+
});
|
|
140
|
+
var EndpointContextPropertiesSchema = BaseEndpointEvaluationPropertiesSchema.partial().extend({
|
|
141
|
+
method: z.string().optional(),
|
|
142
|
+
path: z.string().optional()
|
|
143
|
+
}).describe("Template context for endpoint-specific properties");
|
|
144
|
+
|
|
145
|
+
// src/builders/azure-dashboard-raw/builder.schema.ts
|
|
146
|
+
var EndpointConfigSchema = createEndpointConfigPropertiesSchema({
|
|
147
|
+
availabilityThreshold: DEFAULT_AVAILABILITY_THRESHOLD,
|
|
148
|
+
evaluationFrequency: EVALUATION_FREQUENCY_MINUTES,
|
|
149
|
+
evaluationTimeWindow: TIME_WINDOW_MINUTES,
|
|
150
|
+
eventOccurrences: EVENT_OCCURRENCES,
|
|
151
|
+
responseTimeThreshold: DEFAULT_RESPONSE_TIME_THRESHOLD
|
|
152
|
+
}).extend({
|
|
153
|
+
method: z2.string().optional(),
|
|
154
|
+
path: z2.string().optional()
|
|
155
|
+
});
|
|
156
|
+
var BuilderPropertiesSchema = z2.object({
|
|
157
|
+
endpoints: z2.record(z2.string(), EndpointConfigSchema).optional(),
|
|
158
|
+
evaluation_frequency: z2.number(),
|
|
159
|
+
evaluation_time_window: z2.number(),
|
|
160
|
+
event_occurrences: z2.number(),
|
|
161
|
+
hosts: z2.array(z2.string()).optional(),
|
|
162
|
+
location: z2.string(),
|
|
163
|
+
name: z2.string(),
|
|
164
|
+
resource_ids: z2.array(z2.string()),
|
|
165
|
+
resource_type: z2.enum(["app-gateway", "api-management"]),
|
|
166
|
+
timespan: z2.string()
|
|
167
|
+
});
|
|
168
|
+
var OA3ServerSchema = z2.object({
|
|
169
|
+
description: z2.string().optional(),
|
|
170
|
+
url: z2.string()
|
|
171
|
+
});
|
|
172
|
+
var OA3SpecSchema = z2.object({
|
|
173
|
+
basePath: z2.string().optional(),
|
|
174
|
+
// OA2
|
|
175
|
+
host: z2.string().optional(),
|
|
176
|
+
// OA2
|
|
177
|
+
openapi: z2.string().optional(),
|
|
178
|
+
// OA3
|
|
179
|
+
paths: z2.record(z2.string(), z2.unknown()),
|
|
180
|
+
servers: z2.array(OA3ServerSchema).optional(),
|
|
181
|
+
// OA3
|
|
182
|
+
swagger: z2.string().optional()
|
|
183
|
+
// OA2
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// src/builders/azure-dashboard-raw/endpoints-extractor.ts
|
|
187
|
+
import * as path from "path";
|
|
188
|
+
|
|
189
|
+
// src/core/errors/config-error.ts
|
|
190
|
+
var ConfigError = class _ConfigError extends Error {
|
|
191
|
+
constructor(message) {
|
|
192
|
+
super(message);
|
|
193
|
+
this.name = "ConfigError";
|
|
194
|
+
Object.setPrototypeOf(this, _ConfigError.prototype);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// src/core/errors/file-error.ts
|
|
199
|
+
var FileError = class _FileError extends Error {
|
|
200
|
+
constructor(message) {
|
|
201
|
+
super(message);
|
|
202
|
+
this.name = "FileError";
|
|
203
|
+
Object.setPrototypeOf(this, _FileError.prototype);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// src/core/errors/invalid-builder-error.ts
|
|
208
|
+
var InvalidBuilderError = class _InvalidBuilderError extends Error {
|
|
209
|
+
constructor(message) {
|
|
210
|
+
super(message);
|
|
211
|
+
this.name = "InvalidBuilderError";
|
|
212
|
+
Object.setPrototypeOf(this, _InvalidBuilderError.prototype);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// src/core/errors/parse-error.ts
|
|
217
|
+
var ParseError = class _ParseError extends Error {
|
|
218
|
+
constructor(message) {
|
|
219
|
+
super(message);
|
|
220
|
+
this.name = "ParseError";
|
|
221
|
+
Object.setPrototypeOf(this, _ParseError.prototype);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// src/builders/azure-dashboard-raw/endpoints-extractor.ts
|
|
226
|
+
var VALID_HTTP_METHODS = /* @__PURE__ */ new Set([
|
|
227
|
+
"delete",
|
|
228
|
+
"get",
|
|
229
|
+
"head",
|
|
230
|
+
"options",
|
|
231
|
+
"patch",
|
|
232
|
+
"post",
|
|
233
|
+
"put",
|
|
234
|
+
"trace"
|
|
235
|
+
]);
|
|
236
|
+
function extractEndpoints(oa3Spec, evaluationFrequency, evaluationTimeWindow, eventOccurrences, availabilityThreshold, responseTimeThreshold) {
|
|
237
|
+
const hosts = [];
|
|
238
|
+
const endpoints = {};
|
|
239
|
+
const endpointDefaults = {
|
|
240
|
+
availability_evaluation_frequency: evaluationFrequency,
|
|
241
|
+
availability_evaluation_time_window: evaluationTimeWindow,
|
|
242
|
+
availability_event_occurrences: eventOccurrences,
|
|
243
|
+
availability_threshold: availabilityThreshold ?? DEFAULT_AVAILABILITY_THRESHOLD,
|
|
244
|
+
response_time_evaluation_frequency: evaluationFrequency,
|
|
245
|
+
response_time_evaluation_time_window: evaluationTimeWindow,
|
|
246
|
+
response_time_event_occurrences: eventOccurrences,
|
|
247
|
+
response_time_threshold: responseTimeThreshold ?? DEFAULT_RESPONSE_TIME_THRESHOLD
|
|
248
|
+
};
|
|
249
|
+
const serverUrls = (() => {
|
|
250
|
+
if (oa3Spec.servers && oa3Spec.servers.length > 0) {
|
|
251
|
+
return oa3Spec.servers.map((s) => s.url);
|
|
252
|
+
}
|
|
253
|
+
if (oa3Spec.host) {
|
|
254
|
+
const basePath = oa3Spec.basePath || "";
|
|
255
|
+
return [`${oa3Spec.host}${basePath}`];
|
|
256
|
+
}
|
|
257
|
+
throw new ConfigError(
|
|
258
|
+
'OpenAPI spec must have either "servers" (OA3) or "host" (OA2) defined'
|
|
259
|
+
);
|
|
260
|
+
})();
|
|
261
|
+
if (!oa3Spec.paths || Object.keys(oa3Spec.paths).length === 0) {
|
|
262
|
+
throw new ConfigError(
|
|
263
|
+
"OpenAPI spec has no paths defined. Cannot generate dashboard for empty specification."
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const endpointPaths = Object.keys(oa3Spec.paths);
|
|
267
|
+
const orderedPaths = [];
|
|
268
|
+
for (const serverUrl of serverUrls) {
|
|
269
|
+
const parsedUrl = new URL(
|
|
270
|
+
serverUrl.startsWith("http") ? serverUrl : `https://${serverUrl}`
|
|
271
|
+
);
|
|
272
|
+
const host = parseHost(serverUrl);
|
|
273
|
+
hosts.push(host);
|
|
274
|
+
for (const endpointPath of endpointPaths) {
|
|
275
|
+
const normalizedPath = normalizePath(parsedUrl.pathname, endpointPath);
|
|
276
|
+
const pathItem = oa3Spec.paths[endpointPath];
|
|
277
|
+
const hasValidMethods = pathItem && typeof pathItem === "object" && Object.keys(pathItem).some(
|
|
278
|
+
(method) => VALID_HTTP_METHODS.has(method.toLowerCase())
|
|
279
|
+
);
|
|
280
|
+
if (hasValidMethods && !endpoints[normalizedPath]) {
|
|
281
|
+
endpoints[normalizedPath] = {
|
|
282
|
+
...endpointDefaults
|
|
283
|
+
};
|
|
284
|
+
orderedPaths.push(normalizedPath);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const orderedEndpoints = {};
|
|
289
|
+
for (const path4 of orderedPaths) {
|
|
290
|
+
orderedEndpoints[path4] = endpoints[path4];
|
|
291
|
+
}
|
|
292
|
+
return { endpoints: orderedEndpoints, hosts };
|
|
293
|
+
}
|
|
294
|
+
function normalizePath(urlPath, endpointPath) {
|
|
295
|
+
const cleanEndpointPath = endpointPath.startsWith("/") ? endpointPath.slice(1) : endpointPath;
|
|
296
|
+
const combined = path.posix.join(urlPath || "/", cleanEndpointPath);
|
|
297
|
+
return combined.startsWith("/") ? combined : `/${combined}`;
|
|
298
|
+
}
|
|
299
|
+
function parseHost(hostUrl) {
|
|
300
|
+
try {
|
|
301
|
+
const urlString = hostUrl.startsWith("http") ? hostUrl : `//${hostUrl}`;
|
|
302
|
+
const url = new URL(urlString);
|
|
303
|
+
return url.host;
|
|
304
|
+
} catch {
|
|
305
|
+
return hostUrl.replace(/^\/\//, "").split("/")[0] || hostUrl;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/builders/queries/api-management.ts
|
|
310
|
+
var api_management_exports = {};
|
|
311
|
+
__export(api_management_exports, {
|
|
312
|
+
availabilityQuery: () => availabilityQuery,
|
|
313
|
+
responseCodesQuery: () => responseCodesQuery,
|
|
314
|
+
responseTimeQuery: () => responseTimeQuery
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// src/core/template/helpers.ts
|
|
318
|
+
function uriToRegex(uri) {
|
|
319
|
+
return uri.replace(/\{[^/]+\}/g, "[^/]+") + "$";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/builders/queries/api-management.ts
|
|
323
|
+
function availabilityQuery(ctx) {
|
|
324
|
+
const endpoint = ctx.endpoint;
|
|
325
|
+
const basePath = ctx.base_path ?? "";
|
|
326
|
+
const threshold = ctx.threshold ?? 0.99;
|
|
327
|
+
const props = ctx.endpoints?.[endpoint];
|
|
328
|
+
const method = props?.method;
|
|
329
|
+
const path4 = props?.path ?? endpoint;
|
|
330
|
+
const uriPattern = uriToRegex(basePath + path4);
|
|
331
|
+
const timespan = ctx.timespan || "5m";
|
|
332
|
+
const isAlarm = ctx.is_alarm ?? false;
|
|
333
|
+
const displayThreshold = threshold;
|
|
334
|
+
return `${isAlarm ? "" : "\n"}let threshold = ${displayThreshold};
|
|
335
|
+
AzureDiagnostics
|
|
336
|
+
| where url_s matches regex "${uriPattern}"${method ? `
|
|
337
|
+
| where method_s == "${method}"` : ""}
|
|
338
|
+
| summarize
|
|
339
|
+
Total=count(),
|
|
340
|
+
Success=count(responseCode_d < 500 and responseCode_d != 0) by bin(TimeGenerated, ${timespan})
|
|
341
|
+
| extend availability=toreal(Success) / Total
|
|
342
|
+
${isAlarm ? `| where availability < threshold` : `| project TimeGenerated, availability, watermark=threshold
|
|
343
|
+
| render timechart with (xtitle = "time", ytitle= "availability(%)")`}
|
|
344
|
+
`;
|
|
345
|
+
}
|
|
346
|
+
function responseCodesQuery(ctx) {
|
|
347
|
+
const endpoint = ctx.endpoint;
|
|
348
|
+
const basePath = ctx.base_path ?? "";
|
|
349
|
+
const props = ctx.endpoints?.[endpoint];
|
|
350
|
+
const method = props?.method;
|
|
351
|
+
const path4 = props?.path ?? endpoint;
|
|
352
|
+
const uriPattern = uriToRegex(basePath + path4);
|
|
353
|
+
const timespan = ctx.timespan || "5m";
|
|
354
|
+
return `
|
|
355
|
+
let api_url = "${uriPattern}";
|
|
356
|
+
AzureDiagnostics
|
|
357
|
+
| where url_s matches regex api_url${method ? `
|
|
358
|
+
| where method_s == "${method}"` : ""}
|
|
359
|
+
| extend HTTPStatus = case(
|
|
360
|
+
responseCode_d between (100 .. 199), "1XX",
|
|
361
|
+
responseCode_d between (200 .. 299), "2XX",
|
|
362
|
+
responseCode_d between (300 .. 399), "3XX",
|
|
363
|
+
responseCode_d between (400 .. 499), "4XX",
|
|
364
|
+
"5XX")
|
|
365
|
+
| summarize count() by HTTPStatus, bin(TimeGenerated, ${timespan})
|
|
366
|
+
| render areachart with (xtitle = "time", ytitle= "count")
|
|
367
|
+
`;
|
|
368
|
+
}
|
|
369
|
+
function responseTimeQuery(ctx) {
|
|
370
|
+
const endpoint = ctx.endpoint;
|
|
371
|
+
const basePath = ctx.base_path ?? "";
|
|
372
|
+
const threshold = ctx.threshold ?? 1;
|
|
373
|
+
const props = ctx.endpoints?.[endpoint];
|
|
374
|
+
const method = props?.method;
|
|
375
|
+
const path4 = props?.path ?? endpoint;
|
|
376
|
+
const uriPattern = uriToRegex(basePath + path4);
|
|
377
|
+
const timespan = ctx.timespan || "5m";
|
|
378
|
+
const isAlarm = ctx.is_alarm ?? false;
|
|
379
|
+
const percentile = ctx.queries?.response_time_percentile ?? 95;
|
|
380
|
+
return `${isAlarm ? "" : "\n"}let threshold = ${threshold};
|
|
381
|
+
AzureDiagnostics
|
|
382
|
+
| where url_s matches regex "${uriPattern}"${method ? `
|
|
383
|
+
| where method_s == "${method}"` : ""}
|
|
384
|
+
| summarize
|
|
385
|
+
watermark=threshold,
|
|
386
|
+
duration_percentile_${percentile}=percentiles(todouble(DurationMs)/1000, ${percentile}) by bin(TimeGenerated, ${timespan})
|
|
387
|
+
${isAlarm ? `| where duration_percentile_${percentile} > threshold` : `| render timechart with (xtitle = "time", ytitle= "response time(s)")`}
|
|
388
|
+
`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/builders/queries/app-gateway.ts
|
|
392
|
+
var app_gateway_exports = {};
|
|
393
|
+
__export(app_gateway_exports, {
|
|
394
|
+
availabilityQuery: () => availabilityQuery2,
|
|
395
|
+
responseCodesQuery: () => responseCodesQuery2,
|
|
396
|
+
responseTimeQuery: () => responseTimeQuery2
|
|
397
|
+
});
|
|
398
|
+
function availabilityQuery2(ctx) {
|
|
399
|
+
const endpoint = ctx.endpoint;
|
|
400
|
+
const basePath = ctx.base_path ?? "";
|
|
401
|
+
const threshold = ctx.threshold ?? 0.99;
|
|
402
|
+
const props = ctx.endpoints?.[endpoint];
|
|
403
|
+
const method = props?.method;
|
|
404
|
+
const path4 = props?.path ?? endpoint;
|
|
405
|
+
const uriPattern = uriToRegex(basePath + path4);
|
|
406
|
+
const hostsJson = JSON.stringify(ctx.hosts ?? []).replace(/,/g, ", ");
|
|
407
|
+
const timespan = ctx.timespan || "5m";
|
|
408
|
+
const isAlarm = ctx.is_alarm ?? false;
|
|
409
|
+
const displayThreshold = threshold;
|
|
410
|
+
return `${isAlarm ? "" : "\n"}let api_hosts = datatable (name: string) ${hostsJson};
|
|
411
|
+
let threshold = ${displayThreshold};
|
|
412
|
+
AzureDiagnostics
|
|
413
|
+
| where originalHost_s in (api_hosts)
|
|
414
|
+
| where requestUri_s matches regex "${uriPattern}"${method ? `
|
|
415
|
+
| where httpMethod_s == "${method}"` : ""}
|
|
416
|
+
| summarize
|
|
417
|
+
Total=count(),
|
|
418
|
+
Success=count(httpStatus_d < 500) by bin(TimeGenerated, ${timespan})
|
|
419
|
+
| extend availability=toreal(Success) / Total
|
|
420
|
+
${isAlarm ? "| where availability < threshold" : `| project TimeGenerated, availability, watermark=threshold
|
|
421
|
+
| render timechart with (xtitle = "time", ytitle= "availability(%)")`}
|
|
422
|
+
`;
|
|
423
|
+
}
|
|
424
|
+
function responseCodesQuery2(ctx) {
|
|
425
|
+
const endpoint = ctx.endpoint;
|
|
426
|
+
const basePath = ctx.base_path ?? "";
|
|
427
|
+
const props = ctx.endpoints?.[endpoint];
|
|
428
|
+
const method = props?.method;
|
|
429
|
+
const path4 = props?.path ?? endpoint;
|
|
430
|
+
const uriPattern = uriToRegex(basePath + path4);
|
|
431
|
+
const hostsJson = JSON.stringify(ctx.hosts ?? []).replace(/,/g, ", ");
|
|
432
|
+
const timespan = ctx.timespan || "5m";
|
|
433
|
+
return `
|
|
434
|
+
let api_url = "${uriPattern}";
|
|
435
|
+
let api_hosts = datatable (name: string) ${hostsJson};
|
|
436
|
+
AzureDiagnostics
|
|
437
|
+
| where originalHost_s in (api_hosts)
|
|
438
|
+
| where requestUri_s matches regex api_url${method ? `
|
|
439
|
+
| where httpMethod_s == "${method}"` : ""}
|
|
440
|
+
| extend HTTPStatus = case(
|
|
441
|
+
httpStatus_d between (100 .. 199), "1XX",
|
|
442
|
+
httpStatus_d between (200 .. 299), "2XX",
|
|
443
|
+
httpStatus_d between (300 .. 399), "3XX",
|
|
444
|
+
httpStatus_d between (400 .. 499), "4XX",
|
|
445
|
+
"5XX")
|
|
446
|
+
| summarize count() by HTTPStatus, bin(TimeGenerated, ${timespan})
|
|
447
|
+
| render areachart with (xtitle = "time", ytitle= "count")
|
|
448
|
+
`;
|
|
449
|
+
}
|
|
450
|
+
function responseTimeQuery2(ctx) {
|
|
451
|
+
const endpoint = ctx.endpoint;
|
|
452
|
+
const basePath = ctx.base_path ?? "";
|
|
453
|
+
const threshold = ctx.threshold ?? 1;
|
|
454
|
+
const props = ctx.endpoints?.[endpoint];
|
|
455
|
+
const method = props?.method;
|
|
456
|
+
const path4 = props?.path ?? endpoint;
|
|
457
|
+
const uriPattern = uriToRegex(basePath + path4);
|
|
458
|
+
const hostsJson = JSON.stringify(ctx.hosts ?? []).replace(/,/g, ", ");
|
|
459
|
+
const timespan = ctx.timespan || "5m";
|
|
460
|
+
const isAlarm = ctx.is_alarm ?? false;
|
|
461
|
+
const percentile = ctx.queries?.response_time_percentile ?? 95;
|
|
462
|
+
return `${isAlarm ? "" : "\n"}let api_hosts = datatable (name: string) ${hostsJson};
|
|
463
|
+
let threshold = ${threshold};
|
|
464
|
+
AzureDiagnostics
|
|
465
|
+
| where originalHost_s in (api_hosts)
|
|
466
|
+
| where requestUri_s matches regex "${uriPattern}"${method ? `
|
|
467
|
+
| where httpMethod_s == "${method}"` : ""}
|
|
468
|
+
| summarize
|
|
469
|
+
watermark=threshold,
|
|
470
|
+
duration_percentile_${percentile}=percentiles(timeTaken_d, ${percentile}) by bin(TimeGenerated, ${timespan})
|
|
471
|
+
${isAlarm ? `| where duration_percentile_${percentile} > threshold` : `| render timechart with (xtitle = "time", ytitle= "response time(s)")`}
|
|
472
|
+
`;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/builders/azure-dashboard-raw/template.ts
|
|
476
|
+
function createMetadataInputs(resourceIds, queryValue, partTitle, partSubTitle, specificChart, dimensions) {
|
|
477
|
+
return [
|
|
478
|
+
{ name: "resourceTypeMode", isOptional: true },
|
|
479
|
+
{ name: "ComponentId", isOptional: true },
|
|
480
|
+
{ name: "Scope", value: { resourceIds }, isOptional: true },
|
|
481
|
+
{ name: "PartId", isOptional: true },
|
|
482
|
+
{ name: "Version", value: "2.0", isOptional: true },
|
|
483
|
+
{ name: "TimeRange", value: "PT4H", isOptional: true },
|
|
484
|
+
{ name: "DashboardId", isOptional: true },
|
|
485
|
+
{
|
|
486
|
+
name: "DraftRequestParameters",
|
|
487
|
+
value: { scope: "hierarchy" },
|
|
488
|
+
isOptional: true
|
|
489
|
+
},
|
|
490
|
+
{ name: "Query", value: queryValue, isOptional: true },
|
|
491
|
+
{ name: "ControlType", value: "FrameControlChart", isOptional: true },
|
|
492
|
+
{ name: "SpecificChart", value: specificChart, isOptional: true },
|
|
493
|
+
{ name: "PartTitle", value: partTitle, isOptional: true },
|
|
494
|
+
{ name: "PartSubTitle", value: partSubTitle, isOptional: true },
|
|
495
|
+
{ name: "Dimensions", value: dimensions, isOptional: true },
|
|
496
|
+
{
|
|
497
|
+
name: "LegendOptions",
|
|
498
|
+
value: { isEnabled: true, position: "Bottom" },
|
|
499
|
+
isOptional: true
|
|
500
|
+
},
|
|
501
|
+
{ name: "IsQueryContainTimeRange", value: false, isOptional: true }
|
|
502
|
+
];
|
|
503
|
+
}
|
|
504
|
+
function createAvailabilityPart(ctx, endpoint, props, resourceIds, timespan, fullPath, partIndex, yPosition, queryFns) {
|
|
505
|
+
const availabilityQuery3 = queryFns.availabilityQuery({
|
|
506
|
+
...ctx,
|
|
507
|
+
endpoint,
|
|
508
|
+
is_alarm: false,
|
|
509
|
+
threshold: props.availability_threshold,
|
|
510
|
+
...props
|
|
511
|
+
// Include method and path from queryProps
|
|
512
|
+
});
|
|
513
|
+
return {
|
|
514
|
+
[`${partIndex}`]: {
|
|
515
|
+
position: { x: 0, y: yPosition, colSpan: 6, rowSpan: 4 },
|
|
516
|
+
metadata: {
|
|
517
|
+
inputs: createMetadataInputs(
|
|
518
|
+
resourceIds,
|
|
519
|
+
availabilityQuery3,
|
|
520
|
+
`Availability (${timespan})`,
|
|
521
|
+
fullPath,
|
|
522
|
+
"Line",
|
|
523
|
+
{
|
|
524
|
+
xAxis: { name: "TimeGenerated", type: "datetime" },
|
|
525
|
+
yAxis: [
|
|
526
|
+
{ name: "availability", type: "real" },
|
|
527
|
+
{ name: "watermark", type: "real" }
|
|
528
|
+
],
|
|
529
|
+
splitBy: [],
|
|
530
|
+
aggregation: "Sum"
|
|
531
|
+
}
|
|
532
|
+
),
|
|
533
|
+
type: "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart",
|
|
534
|
+
settings: {
|
|
535
|
+
content: {
|
|
536
|
+
Query: availabilityQuery3,
|
|
537
|
+
PartTitle: `Availability (${timespan})`
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function createResponseCodesPart(ctx, endpoint, queryProps, resourceIds, timespan, fullPath, partIndex, yPosition, queryFns) {
|
|
545
|
+
const responseCodesQuery3 = queryFns.responseCodesQuery({
|
|
546
|
+
...ctx,
|
|
547
|
+
endpoint,
|
|
548
|
+
...queryProps
|
|
549
|
+
// Include method and path
|
|
550
|
+
});
|
|
551
|
+
return {
|
|
552
|
+
[`${partIndex}`]: {
|
|
553
|
+
position: { x: 6, y: yPosition, colSpan: 6, rowSpan: 4 },
|
|
554
|
+
metadata: {
|
|
555
|
+
inputs: createMetadataInputs(
|
|
556
|
+
resourceIds,
|
|
557
|
+
responseCodesQuery3,
|
|
558
|
+
`Response Codes (${timespan})`,
|
|
559
|
+
fullPath,
|
|
560
|
+
"Pie",
|
|
561
|
+
{
|
|
562
|
+
xAxis: { name: "httpStatus_d", type: "string" },
|
|
563
|
+
yAxis: [{ name: "count_", type: "long" }],
|
|
564
|
+
splitBy: [],
|
|
565
|
+
aggregation: "Sum"
|
|
566
|
+
}
|
|
567
|
+
),
|
|
568
|
+
type: "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart",
|
|
569
|
+
settings: {
|
|
570
|
+
content: {
|
|
571
|
+
Query: responseCodesQuery3,
|
|
572
|
+
SpecificChart: "StackedArea",
|
|
573
|
+
PartTitle: `Response Codes (${timespan})`,
|
|
574
|
+
Dimensions: {
|
|
575
|
+
xAxis: { name: "TimeGenerated", type: "datetime" },
|
|
576
|
+
yAxis: [{ name: "count_", type: "long" }],
|
|
577
|
+
splitBy: [{ name: "HTTPStatus", type: "string" }],
|
|
578
|
+
aggregation: "Sum"
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function createResponseTimePart(ctx, endpoint, props, resourceIds, timespan, fullPath, partIndex, yPosition, queryFns) {
|
|
587
|
+
const responseTimeQuery3 = queryFns.responseTimeQuery({
|
|
588
|
+
...ctx,
|
|
589
|
+
endpoint,
|
|
590
|
+
is_alarm: false,
|
|
591
|
+
threshold: props.response_time_threshold,
|
|
592
|
+
...props
|
|
593
|
+
// Include method and path from queryProps
|
|
594
|
+
});
|
|
595
|
+
return {
|
|
596
|
+
[`${partIndex}`]: {
|
|
597
|
+
position: { x: 12, y: yPosition, colSpan: 6, rowSpan: 4 },
|
|
598
|
+
metadata: {
|
|
599
|
+
inputs: createMetadataInputs(
|
|
600
|
+
resourceIds,
|
|
601
|
+
responseTimeQuery3,
|
|
602
|
+
`Percentile Response Time (${timespan})`,
|
|
603
|
+
fullPath,
|
|
604
|
+
"StackedColumn",
|
|
605
|
+
{
|
|
606
|
+
xAxis: { name: "TimeGenerated", type: "datetime" },
|
|
607
|
+
yAxis: [{ name: "duration_percentile_95", type: "real" }],
|
|
608
|
+
splitBy: [],
|
|
609
|
+
aggregation: "Sum"
|
|
610
|
+
}
|
|
611
|
+
),
|
|
612
|
+
type: "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart",
|
|
613
|
+
settings: {
|
|
614
|
+
content: {
|
|
615
|
+
Query: responseTimeQuery3,
|
|
616
|
+
SpecificChart: "Line",
|
|
617
|
+
PartTitle: `Percentile Response Time (${timespan})`,
|
|
618
|
+
Dimensions: {
|
|
619
|
+
xAxis: { name: "TimeGenerated", type: "datetime" },
|
|
620
|
+
yAxis: [
|
|
621
|
+
{ name: "watermark", type: "long" },
|
|
622
|
+
{ name: "duration_percentile_95", type: "real" }
|
|
623
|
+
],
|
|
624
|
+
splitBy: [],
|
|
625
|
+
aggregation: "Sum"
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function azureDashboardRawTemplate(context) {
|
|
634
|
+
const basePath = context.base_path ?? "";
|
|
635
|
+
const resourceIds = [context.data_source_id];
|
|
636
|
+
const timespan = context.timespan || "5m";
|
|
637
|
+
const queryFns = context.resource_type === "api-management" ? api_management_exports : app_gateway_exports;
|
|
638
|
+
const endpointEntries = Object.entries(context.endpoints);
|
|
639
|
+
const parts = endpointEntries.flatMap(([endpoint, props], i) => {
|
|
640
|
+
const parsed = parseEndpointKey(endpoint);
|
|
641
|
+
const fullPath = basePath + endpoint;
|
|
642
|
+
const queryProps = {
|
|
643
|
+
...props,
|
|
644
|
+
method: parsed.method || props.method,
|
|
645
|
+
path: parsed.path || props.path || endpoint
|
|
646
|
+
};
|
|
647
|
+
const partIndex = i * 3;
|
|
648
|
+
const yPosition = i * 4;
|
|
649
|
+
return [
|
|
650
|
+
createAvailabilityPart(
|
|
651
|
+
context,
|
|
652
|
+
endpoint,
|
|
653
|
+
queryProps,
|
|
654
|
+
resourceIds,
|
|
655
|
+
timespan,
|
|
656
|
+
fullPath,
|
|
657
|
+
partIndex + 0,
|
|
658
|
+
yPosition,
|
|
659
|
+
queryFns
|
|
660
|
+
),
|
|
661
|
+
createResponseCodesPart(
|
|
662
|
+
context,
|
|
663
|
+
endpoint,
|
|
664
|
+
queryProps,
|
|
665
|
+
resourceIds,
|
|
666
|
+
timespan,
|
|
667
|
+
fullPath,
|
|
668
|
+
partIndex + 1,
|
|
669
|
+
yPosition,
|
|
670
|
+
queryFns
|
|
671
|
+
),
|
|
672
|
+
createResponseTimePart(
|
|
673
|
+
context,
|
|
674
|
+
endpoint,
|
|
675
|
+
queryProps,
|
|
676
|
+
resourceIds,
|
|
677
|
+
timespan,
|
|
678
|
+
fullPath,
|
|
679
|
+
partIndex + 2,
|
|
680
|
+
yPosition,
|
|
681
|
+
queryFns
|
|
682
|
+
)
|
|
683
|
+
];
|
|
684
|
+
});
|
|
685
|
+
const mergedParts = Object.assign({}, ...parts);
|
|
686
|
+
const baseUuid = "9badbd78-7607-4131-8fa1-8b85191432";
|
|
687
|
+
const maxFilteredParts = 9;
|
|
688
|
+
const filteredPartIds = Array.from({ length: maxFilteredParts }, (_, i) => {
|
|
689
|
+
const hex = (237 + i * 2).toString(16);
|
|
690
|
+
return `StartboardPart-LogsDashboardPart-${baseUuid}${hex}`;
|
|
691
|
+
});
|
|
692
|
+
const dashboard = {
|
|
693
|
+
properties: {
|
|
694
|
+
lenses: {
|
|
695
|
+
"0": {
|
|
696
|
+
order: 0,
|
|
697
|
+
parts: mergedParts
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
metadata: {
|
|
701
|
+
model: {
|
|
702
|
+
timeRange: {
|
|
703
|
+
value: {
|
|
704
|
+
relative: {
|
|
705
|
+
duration: 24,
|
|
706
|
+
timeUnit: 1
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
type: "MsPortalFx.Composition.Configuration.ValueTypes.TimeRange"
|
|
710
|
+
},
|
|
711
|
+
filterLocale: {
|
|
712
|
+
value: "en-us"
|
|
713
|
+
},
|
|
714
|
+
filters: {
|
|
715
|
+
value: {
|
|
716
|
+
MsPortalFx_TimeRange: {
|
|
717
|
+
model: {
|
|
718
|
+
format: "local",
|
|
719
|
+
granularity: "auto",
|
|
720
|
+
relative: "48h"
|
|
721
|
+
},
|
|
722
|
+
displayCache: {
|
|
723
|
+
name: "Local Time",
|
|
724
|
+
value: "Past 48 hours"
|
|
725
|
+
},
|
|
726
|
+
filteredPartIds
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
},
|
|
733
|
+
name: context.name,
|
|
734
|
+
type: "Microsoft.Portal/dashboards",
|
|
735
|
+
location: context.location,
|
|
736
|
+
tags: {
|
|
737
|
+
"hidden-title": context.name
|
|
738
|
+
},
|
|
739
|
+
apiVersion: "2015-08-01-preview"
|
|
740
|
+
};
|
|
741
|
+
return JSON.stringify(dashboard, null, 2);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/builders/azure-dashboard-raw/builder.ts
|
|
745
|
+
var AzDashboardRawBuilder = class extends Builder {
|
|
746
|
+
evaluationFrequency;
|
|
747
|
+
evaluationTimeWindow;
|
|
748
|
+
eventOccurrences;
|
|
749
|
+
oa3Spec;
|
|
750
|
+
constructor(options) {
|
|
751
|
+
super(azureDashboardRawTemplate, {
|
|
752
|
+
action_groups_ids: [],
|
|
753
|
+
availability_threshold: options.availabilityThreshold,
|
|
754
|
+
data_source_id: options.resources[0],
|
|
755
|
+
endpoints: {},
|
|
756
|
+
evaluation_frequency: options.evaluationFrequency,
|
|
757
|
+
event_occurrences: options.eventOccurrences,
|
|
758
|
+
hosts: [],
|
|
759
|
+
location: options.location,
|
|
760
|
+
name: options.name,
|
|
761
|
+
queries: options.queries,
|
|
762
|
+
resource_type: options.resourceType,
|
|
763
|
+
response_time_threshold: options.responseTimeThreshold,
|
|
764
|
+
time_window: options.evaluationTimeWindow,
|
|
765
|
+
timespan: options.timespan
|
|
766
|
+
});
|
|
767
|
+
this.oa3Spec = OA3SpecSchema.parse(options.oa3Spec);
|
|
768
|
+
this.evaluationFrequency = options.evaluationFrequency;
|
|
769
|
+
this.evaluationTimeWindow = options.evaluationTimeWindow;
|
|
770
|
+
this.eventOccurrences = options.eventOccurrences;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Render the template by extracting endpoints from OA3 spec and merging with overrides.
|
|
774
|
+
*/
|
|
775
|
+
produce(values = {}) {
|
|
776
|
+
const { endpoints, hosts } = extractEndpoints(
|
|
777
|
+
this.oa3Spec,
|
|
778
|
+
this.evaluationFrequency,
|
|
779
|
+
this.evaluationTimeWindow,
|
|
780
|
+
this.eventOccurrences,
|
|
781
|
+
this.properties.availability_threshold,
|
|
782
|
+
this.properties.response_time_threshold
|
|
783
|
+
);
|
|
784
|
+
this.properties.hosts = hosts;
|
|
785
|
+
this.properties.endpoints = endpoints;
|
|
786
|
+
const mergedValues = values.endpoints ? { ...values, endpoints: normalizeEndpointKeys(values.endpoints) } : values;
|
|
787
|
+
return super.produce(mergedValues);
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
// src/builders/azure-dashboard/builder.ts
|
|
792
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
793
|
+
import * as path3 from "path";
|
|
794
|
+
|
|
795
|
+
// src/builders/azure-dashboard/packager.ts
|
|
796
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
797
|
+
import * as path2 from "path";
|
|
798
|
+
|
|
799
|
+
// src/builders/azure-dashboard/terraform-assets.ts
|
|
800
|
+
function generateBackendTfvars(backend) {
|
|
801
|
+
return `resource_group_name = ${backend?.resource_group_name ? `"${backend.resource_group_name}"` : ""}
|
|
802
|
+
storage_account_name = ${backend?.storage_account_name ? `"${backend.storage_account_name}"` : ""}
|
|
803
|
+
container_name = ${backend?.container_name ? `"${backend.container_name}"` : ""}
|
|
804
|
+
key = ${backend?.key ? `"${backend.key}"` : ""}
|
|
805
|
+
use_azuread_auth = "true"
|
|
806
|
+
`;
|
|
807
|
+
}
|
|
808
|
+
function generateMainTf() {
|
|
809
|
+
const terraformVersion = ">=1.1.5";
|
|
810
|
+
const azurermVersion = ">= 3.86.0, <=3.116.0";
|
|
811
|
+
return `terraform {
|
|
812
|
+
required_version = "${terraformVersion}"
|
|
813
|
+
|
|
814
|
+
required_providers {
|
|
815
|
+
azurerm = {
|
|
816
|
+
source = "hashicorp/azurerm"
|
|
817
|
+
version = "${azurermVersion}"
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
backend "azurerm" {}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
provider "azurerm" {
|
|
825
|
+
features {}
|
|
826
|
+
}
|
|
827
|
+
`;
|
|
828
|
+
}
|
|
829
|
+
function generateTerraformTfvars(envConfig) {
|
|
830
|
+
return `prefix = ${envConfig?.prefix ? `"${envConfig.prefix}"` : ""}
|
|
831
|
+
env_short = ${envConfig?.env_short ? `"${envConfig.env_short}"` : ""}
|
|
832
|
+
`;
|
|
833
|
+
}
|
|
834
|
+
function generateVariablesTf() {
|
|
835
|
+
return `variable "prefix" {
|
|
836
|
+
type = string
|
|
837
|
+
validation {
|
|
838
|
+
condition = (
|
|
839
|
+
length(var.prefix) <= 6
|
|
840
|
+
)
|
|
841
|
+
error_message = "Max length is 6 chars."
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
variable "env_short" {
|
|
846
|
+
type = string
|
|
847
|
+
validation {
|
|
848
|
+
condition = (
|
|
849
|
+
length(var.env_short) <= 1
|
|
850
|
+
)
|
|
851
|
+
error_message = "Max length is 1 chars."
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
variable "tags" {
|
|
856
|
+
type = map(any)
|
|
857
|
+
default = {
|
|
858
|
+
CreatedBy = "Terraform"
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
`;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// src/builders/azure-dashboard/packager.ts
|
|
865
|
+
async function generateTerraformAssets(outputPath, terraformConfig) {
|
|
866
|
+
try {
|
|
867
|
+
const mainTfContent = generateMainTf();
|
|
868
|
+
const variablesTfContent = generateVariablesTf();
|
|
869
|
+
await Promise.all([
|
|
870
|
+
writeFile(path2.join(outputPath, "main.tf"), mainTfContent, "utf-8"),
|
|
871
|
+
writeFile(
|
|
872
|
+
path2.join(outputPath, "variables.tf"),
|
|
873
|
+
variablesTfContent,
|
|
874
|
+
"utf-8"
|
|
875
|
+
)
|
|
876
|
+
]);
|
|
877
|
+
if (terraformConfig && "prefix" in terraformConfig) {
|
|
878
|
+
const backendTfvarsContent = generateBackendTfvars(
|
|
879
|
+
terraformConfig.backend
|
|
880
|
+
);
|
|
881
|
+
const terraformTfvarsContent = generateTerraformTfvars({
|
|
882
|
+
backend: terraformConfig.backend,
|
|
883
|
+
env_short: terraformConfig.env_short,
|
|
884
|
+
prefix: terraformConfig.prefix
|
|
885
|
+
});
|
|
886
|
+
await Promise.all([
|
|
887
|
+
writeFile(
|
|
888
|
+
path2.join(outputPath, "backend.tfvars"),
|
|
889
|
+
backendTfvarsContent,
|
|
890
|
+
"utf-8"
|
|
891
|
+
),
|
|
892
|
+
writeFile(
|
|
893
|
+
path2.join(outputPath, "terraform.tfvars"),
|
|
894
|
+
terraformTfvarsContent,
|
|
895
|
+
"utf-8"
|
|
896
|
+
)
|
|
897
|
+
]);
|
|
898
|
+
} else if (terraformConfig && "environments" in terraformConfig) {
|
|
899
|
+
const envPromises = [];
|
|
900
|
+
for (const [env, envConfig] of Object.entries(
|
|
901
|
+
terraformConfig.environments
|
|
902
|
+
)) {
|
|
903
|
+
if (envConfig) {
|
|
904
|
+
const envPath = path2.join(outputPath, "env", env);
|
|
905
|
+
const envPromise = (async () => {
|
|
906
|
+
await mkdir(envPath, { recursive: true });
|
|
907
|
+
const backendTfvarsContent = generateBackendTfvars(
|
|
908
|
+
envConfig.backend
|
|
909
|
+
);
|
|
910
|
+
const terraformTfvarsContent = generateTerraformTfvars(envConfig);
|
|
911
|
+
await Promise.all([
|
|
912
|
+
writeFile(
|
|
913
|
+
path2.join(envPath, "backend.tfvars"),
|
|
914
|
+
backendTfvarsContent,
|
|
915
|
+
"utf-8"
|
|
916
|
+
),
|
|
917
|
+
writeFile(
|
|
918
|
+
path2.join(envPath, "terraform.tfvars"),
|
|
919
|
+
terraformTfvarsContent,
|
|
920
|
+
"utf-8"
|
|
921
|
+
)
|
|
922
|
+
]);
|
|
923
|
+
})();
|
|
924
|
+
envPromises.push(envPromise);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
await Promise.all(envPromises);
|
|
928
|
+
}
|
|
929
|
+
} catch (error) {
|
|
930
|
+
throw new FileError(
|
|
931
|
+
`Failed to generate Terraform assets in ${outputPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// src/builders/azure-dashboard/template.ts
|
|
937
|
+
function azureDashboardTerraformTemplate(context) {
|
|
938
|
+
const name = context.name;
|
|
939
|
+
const dashboardProperties = context.dashboard_properties || "";
|
|
940
|
+
const basePath = context.base_path ?? "";
|
|
941
|
+
const actionGroupsJson = JSON.stringify(context.action_groups_ids).replace(
|
|
942
|
+
/,/g,
|
|
943
|
+
", "
|
|
944
|
+
);
|
|
945
|
+
const dataSourceId = context.data_source_id;
|
|
946
|
+
const queryFns = context.resource_type === "api-management" ? api_management_exports : app_gateway_exports;
|
|
947
|
+
return `
|
|
948
|
+
locals {
|
|
949
|
+
name = "\${var.prefix}-\${var.env_short}-${name}"
|
|
950
|
+
dashboard_base_addr = "https://portal.azure.com/#@pagopait.onmicrosoft.com/dashboard/arm"
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
data "azurerm_resource_group" "this" {
|
|
954
|
+
name = "dashboards"
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
resource "azurerm_portal_dashboard" "this" {
|
|
958
|
+
name = local.name
|
|
959
|
+
resource_group_name = data.azurerm_resource_group.this.name
|
|
960
|
+
location = data.azurerm_resource_group.this.location
|
|
961
|
+
|
|
962
|
+
dashboard_properties = <<-PROPS
|
|
963
|
+
${dashboardProperties}
|
|
964
|
+
PROPS
|
|
965
|
+
|
|
966
|
+
tags = var.tags
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
${Object.entries(context.endpoints).map(([endpoint, props], i) => {
|
|
971
|
+
const fullPath = basePath + endpoint;
|
|
972
|
+
const availabilityQuery3 = queryFns.availabilityQuery({
|
|
973
|
+
...context,
|
|
974
|
+
endpoint,
|
|
975
|
+
is_alarm: true,
|
|
976
|
+
threshold: props.availability_threshold,
|
|
977
|
+
...props
|
|
978
|
+
});
|
|
979
|
+
const responseTimeQuery3 = queryFns.responseTimeQuery({
|
|
980
|
+
...context,
|
|
981
|
+
endpoint,
|
|
982
|
+
is_alarm: true,
|
|
983
|
+
threshold: props.response_time_threshold,
|
|
984
|
+
...props
|
|
985
|
+
});
|
|
986
|
+
return `resource "azurerm_monitor_scheduled_query_rules_alert" "alarm_availability_${i}" {
|
|
987
|
+
name = replace(join("_",split("/", "\${local.name}-availability @ ${fullPath}")), "/\\\\{|\\\\}/", "")
|
|
988
|
+
resource_group_name = data.azurerm_resource_group.this.name
|
|
989
|
+
location = data.azurerm_resource_group.this.location
|
|
990
|
+
|
|
991
|
+
action {
|
|
992
|
+
action_group = ${actionGroupsJson}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
data_source_id = "${dataSourceId}"
|
|
996
|
+
description = "Availability for ${fullPath} is less than or equal to 99% - \${local.dashboard_base_addr}\${azurerm_portal_dashboard.this.id}"
|
|
997
|
+
enabled = true
|
|
998
|
+
auto_mitigation_enabled = false
|
|
999
|
+
|
|
1000
|
+
query = <<-QUERY
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
${availabilityQuery3}
|
|
1004
|
+
|
|
1005
|
+
QUERY
|
|
1006
|
+
|
|
1007
|
+
severity = 1
|
|
1008
|
+
frequency = ${props.availability_evaluation_frequency ?? 10}
|
|
1009
|
+
time_window = ${props.availability_evaluation_time_window ?? 20}
|
|
1010
|
+
trigger {
|
|
1011
|
+
operator = "GreaterThanOrEqual"
|
|
1012
|
+
threshold = ${props.availability_event_occurrences ?? 1}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
tags = var.tags
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
resource "azurerm_monitor_scheduled_query_rules_alert" "alarm_time_${i}" {
|
|
1019
|
+
name = replace(join("_",split("/", "\${local.name}-responsetime @ ${fullPath}")), "/\\\\{|\\\\}/", "")
|
|
1020
|
+
resource_group_name = data.azurerm_resource_group.this.name
|
|
1021
|
+
location = data.azurerm_resource_group.this.location
|
|
1022
|
+
|
|
1023
|
+
action {
|
|
1024
|
+
action_group = ${actionGroupsJson}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
data_source_id = "${dataSourceId}"
|
|
1028
|
+
description = "Response time for ${fullPath} is less than or equal to 1s - \${local.dashboard_base_addr}\${azurerm_portal_dashboard.this.id}"
|
|
1029
|
+
enabled = true
|
|
1030
|
+
auto_mitigation_enabled = false
|
|
1031
|
+
|
|
1032
|
+
query = <<-QUERY
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
${responseTimeQuery3}
|
|
1036
|
+
|
|
1037
|
+
QUERY
|
|
1038
|
+
|
|
1039
|
+
severity = 1
|
|
1040
|
+
frequency = ${props.response_time_evaluation_frequency ?? 10}
|
|
1041
|
+
time_window = ${props.response_time_evaluation_time_window ?? 20}
|
|
1042
|
+
trigger {
|
|
1043
|
+
operator = "GreaterThanOrEqual"
|
|
1044
|
+
threshold = ${props.response_time_event_occurrences ?? 1}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
tags = var.tags
|
|
1048
|
+
}
|
|
1049
|
+
`;
|
|
1050
|
+
}).join("\n")}
|
|
1051
|
+
`;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// src/builders/azure-dashboard/builder.ts
|
|
1055
|
+
var AzDashboardBuilder = class extends Builder {
|
|
1056
|
+
rawBuilder;
|
|
1057
|
+
terraformConfig;
|
|
1058
|
+
constructor(options) {
|
|
1059
|
+
super(azureDashboardTerraformTemplate, {
|
|
1060
|
+
action_groups_ids: options.actionGroupsIds,
|
|
1061
|
+
data_source_id: options.dataSourceId,
|
|
1062
|
+
endpoints: {},
|
|
1063
|
+
evaluation_frequency: options.evaluationFrequency,
|
|
1064
|
+
event_occurrences: options.eventOccurrences,
|
|
1065
|
+
hosts: [],
|
|
1066
|
+
location: options.location,
|
|
1067
|
+
name: options.name.replace(/ /g, "_"),
|
|
1068
|
+
// Replace spaces with underscores for Terraform compatibility
|
|
1069
|
+
resource_type: options.resourceType,
|
|
1070
|
+
time_window: options.evaluationTimeWindow,
|
|
1071
|
+
timespan: options.timespan
|
|
1072
|
+
});
|
|
1073
|
+
this.rawBuilder = options.dashboardBuilder;
|
|
1074
|
+
this.terraformConfig = options.terraformConfig;
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Package Terraform configuration.
|
|
1078
|
+
* Creates opex.tf and generates terraform assets (main.tf, variables.tf, env/).
|
|
1079
|
+
*/
|
|
1080
|
+
async package(outputPath, values = {}) {
|
|
1081
|
+
try {
|
|
1082
|
+
await mkdir2(outputPath, { recursive: true });
|
|
1083
|
+
const terraformFilePath = path3.join(outputPath, "opex.tf");
|
|
1084
|
+
const content = this.produce(values);
|
|
1085
|
+
await writeFile2(terraformFilePath, content, "utf-8");
|
|
1086
|
+
await generateTerraformAssets(outputPath, this.terraformConfig);
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
throw new FileError(
|
|
1089
|
+
`Failed to package Terraform configuration in ${outputPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Render Terraform template with embedded dashboard JSON from raw builder.
|
|
1095
|
+
*/
|
|
1096
|
+
produce(values = {}) {
|
|
1097
|
+
const normalizedValues = values.endpoints ? { ...values, endpoints: normalizeEndpointKeys(values.endpoints) } : values;
|
|
1098
|
+
const rawJson = this.rawBuilder.produce(normalizedValues);
|
|
1099
|
+
const dashboard = JSON.parse(rawJson);
|
|
1100
|
+
this.properties.dashboard_properties = JSON.stringify(
|
|
1101
|
+
dashboard.properties,
|
|
1102
|
+
null,
|
|
1103
|
+
2
|
|
1104
|
+
);
|
|
1105
|
+
const rawProps = this.rawBuilder.props();
|
|
1106
|
+
this.properties.hosts = rawProps.hosts;
|
|
1107
|
+
this.properties.endpoints = rawProps.endpoints;
|
|
1108
|
+
return super.produce(normalizedValues);
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
// src/core/builder-factory.ts
|
|
1113
|
+
async function createAzureRawBuilder(params) {
|
|
1114
|
+
const oa3Spec = await params.resolver.resolve();
|
|
1115
|
+
return new AzDashboardRawBuilder({
|
|
1116
|
+
availabilityThreshold: params.availability_threshold,
|
|
1117
|
+
evaluationFrequency: params.evaluation_frequency,
|
|
1118
|
+
evaluationTimeWindow: params.evaluation_time_window,
|
|
1119
|
+
eventOccurrences: params.event_occurrences,
|
|
1120
|
+
location: params.location,
|
|
1121
|
+
name: params.name,
|
|
1122
|
+
oa3Spec,
|
|
1123
|
+
queries: params.queries,
|
|
1124
|
+
resources: params.resources,
|
|
1125
|
+
resourceType: params.resource_type,
|
|
1126
|
+
responseTimeThreshold: params.response_time_threshold,
|
|
1127
|
+
timespan: params.timespan
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
async function createAzureTerraformBuilder(params) {
|
|
1131
|
+
const rawBuilder = await createAzureRawBuilder(params);
|
|
1132
|
+
return new AzDashboardBuilder({
|
|
1133
|
+
actionGroupsIds: params.action_groups_ids,
|
|
1134
|
+
dashboardBuilder: rawBuilder,
|
|
1135
|
+
dataSourceId: params.data_source_id,
|
|
1136
|
+
evaluationFrequency: params.evaluation_frequency,
|
|
1137
|
+
evaluationTimeWindow: params.evaluation_time_window,
|
|
1138
|
+
eventOccurrences: params.event_occurrences,
|
|
1139
|
+
location: params.location,
|
|
1140
|
+
name: params.name,
|
|
1141
|
+
resourceType: params.resource_type,
|
|
1142
|
+
terraformConfig: params.terraform,
|
|
1143
|
+
timespan: params.timespan
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
var builderRegistry = {
|
|
1147
|
+
"azure-dashboard": createAzureTerraformBuilder,
|
|
1148
|
+
"azure-dashboard-raw": createAzureRawBuilder
|
|
1149
|
+
};
|
|
1150
|
+
async function createBuilder(templateType, params) {
|
|
1151
|
+
const factory = builderRegistry[templateType];
|
|
1152
|
+
if (!factory) {
|
|
1153
|
+
throw new InvalidBuilderError(
|
|
1154
|
+
`Invalid builder error: unknown builder ${templateType}`
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
try {
|
|
1158
|
+
return await factory(params);
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
throw new InvalidBuilderError(
|
|
1161
|
+
`Failed to create builder: ${error instanceof Error ? error.message : String(error)}`
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// src/core/config/config.schema.ts
|
|
1167
|
+
import { z as z4 } from "zod";
|
|
1168
|
+
|
|
1169
|
+
// src/core/shared/query-config.schema.ts
|
|
1170
|
+
import { z as z3 } from "zod";
|
|
1171
|
+
var QueryConfigSchema = z3.object({
|
|
1172
|
+
response_time_percentile: z3.number().default(95).describe("Percentile for response time queries. Default: 95"),
|
|
1173
|
+
status_code_categories: z3.array(z3.string()).default(["1XX", "2XX", "3XX", "4XX", "5XX"]).describe("HTTP status code categories for response codes queries")
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
// src/core/config/defaults.ts
|
|
1177
|
+
var DEFAULT_TIMESPAN = "5m";
|
|
1178
|
+
var DEFAULTS = {
|
|
1179
|
+
availability_threshold: DEFAULT_AVAILABILITY_THRESHOLD,
|
|
1180
|
+
evaluation_frequency: EVALUATION_FREQUENCY_MINUTES,
|
|
1181
|
+
evaluation_time_window: TIME_WINDOW_MINUTES,
|
|
1182
|
+
event_occurrences: EVENT_OCCURRENCES,
|
|
1183
|
+
response_time_threshold: DEFAULT_RESPONSE_TIME_THRESHOLD,
|
|
1184
|
+
timespan: DEFAULT_TIMESPAN
|
|
1185
|
+
};
|
|
1186
|
+
|
|
1187
|
+
// src/core/config/config.schema.ts
|
|
1188
|
+
var EndpointOverrideSchema = EndpointOverridePropertiesSchema.extend({
|
|
1189
|
+
availability_evaluation_frequency: z4.number().optional().describe(
|
|
1190
|
+
"Frequency in minutes to evaluate availability alarm. Default: 10"
|
|
1191
|
+
),
|
|
1192
|
+
availability_evaluation_time_window: z4.number().optional().describe(
|
|
1193
|
+
"Time window in minutes for availability alarm evaluation. Default: 20"
|
|
1194
|
+
),
|
|
1195
|
+
availability_event_occurrences: z4.number().optional().describe(
|
|
1196
|
+
"Number of event occurrences to trigger availability alarm. Default: 1"
|
|
1197
|
+
),
|
|
1198
|
+
availability_threshold: z4.number().optional().describe("Minimum availability percentage (0-1). Default: 0.99 (99%)"),
|
|
1199
|
+
response_time_evaluation_frequency: z4.number().optional().describe(
|
|
1200
|
+
"Frequency in minutes to evaluate response time alarm. Default: 10"
|
|
1201
|
+
),
|
|
1202
|
+
response_time_evaluation_time_window: z4.number().optional().describe(
|
|
1203
|
+
"Time window in minutes for response time alarm evaluation. Default: 20"
|
|
1204
|
+
),
|
|
1205
|
+
response_time_event_occurrences: z4.number().optional().describe(
|
|
1206
|
+
"Number of event occurrences to trigger response time alarm. Default: 1"
|
|
1207
|
+
),
|
|
1208
|
+
response_time_threshold: z4.number().optional().describe("Maximum response time in seconds. Default: 1")
|
|
1209
|
+
});
|
|
1210
|
+
var OverridesSchema = z4.object({
|
|
1211
|
+
endpoints: z4.record(z4.string(), EndpointOverrideSchema).optional().describe(
|
|
1212
|
+
"Override alarm thresholds and settings for specific endpoints (key: endpoint path)"
|
|
1213
|
+
),
|
|
1214
|
+
hosts: z4.array(z4.string()).optional().describe(
|
|
1215
|
+
"Override host URLs from OpenAPI spec (e.g., https://example.com)"
|
|
1216
|
+
),
|
|
1217
|
+
queries: QueryConfigSchema.optional().describe(
|
|
1218
|
+
"Optional query configuration overrides"
|
|
1219
|
+
)
|
|
1220
|
+
});
|
|
1221
|
+
var BackendConfigSchema = z4.object({
|
|
1222
|
+
container_name: z4.string().describe("Blob container name for Terraform state"),
|
|
1223
|
+
key: z4.string().describe("State file key/path"),
|
|
1224
|
+
resource_group_name: z4.string().describe("Azure resource group for backend state"),
|
|
1225
|
+
storage_account_name: z4.string().describe("Storage account for Terraform state")
|
|
1226
|
+
});
|
|
1227
|
+
var EnvironmentConfigSchema = z4.object({
|
|
1228
|
+
backend: BackendConfigSchema.optional().describe(
|
|
1229
|
+
"Azure backend configuration for Terraform state"
|
|
1230
|
+
),
|
|
1231
|
+
env_short: z4.string().max(1).describe("Environment short name (1 char: 'd'=dev, 'u'=uat, 'p'=prod)"),
|
|
1232
|
+
prefix: z4.string().max(6).describe("Project prefix (max 6 chars, e.g., 'io', 'pagopa')")
|
|
1233
|
+
});
|
|
1234
|
+
var TerraformEnvironmentsConfigSchema = z4.object({
|
|
1235
|
+
environments: z4.object({
|
|
1236
|
+
dev: EnvironmentConfigSchema.optional(),
|
|
1237
|
+
prod: EnvironmentConfigSchema.optional(),
|
|
1238
|
+
uat: EnvironmentConfigSchema.optional()
|
|
1239
|
+
}).describe("Environment-specific configurations for dev/uat/prod")
|
|
1240
|
+
}).strict();
|
|
1241
|
+
var TerraformConfigSchema = z4.union([
|
|
1242
|
+
EnvironmentConfigSchema.strict(),
|
|
1243
|
+
TerraformEnvironmentsConfigSchema
|
|
1244
|
+
]);
|
|
1245
|
+
var ConfigSchema = z4.object({
|
|
1246
|
+
action_groups: z4.array(z4.string()).describe(
|
|
1247
|
+
"Array of Azure Action Group resource IDs for alarm notifications"
|
|
1248
|
+
),
|
|
1249
|
+
availability_threshold: z4.number().optional().default(DEFAULTS.availability_threshold).describe(
|
|
1250
|
+
"Default minimum availability percentage (0-1). Default: 0.99 (99%)"
|
|
1251
|
+
),
|
|
1252
|
+
data_source: z4.string().describe(
|
|
1253
|
+
"Azure resource ID for metrics data source (Application Gateway or API Management)"
|
|
1254
|
+
),
|
|
1255
|
+
evaluation_frequency: z4.number().optional().default(DEFAULTS.evaluation_frequency).describe("Default frequency in minutes to evaluate alarms. Default: 10"),
|
|
1256
|
+
evaluation_time_window: z4.number().optional().default(DEFAULTS.evaluation_time_window).describe(
|
|
1257
|
+
"Default time window in minutes for alarm evaluation. Default: 20"
|
|
1258
|
+
),
|
|
1259
|
+
event_occurrences: z4.number().optional().default(DEFAULTS.event_occurrences).describe(
|
|
1260
|
+
"Default number of event occurrences to trigger an alarm. Default: 1"
|
|
1261
|
+
),
|
|
1262
|
+
location: z4.string().describe("Azure region/location for the dashboard (e.g., West Europe)"),
|
|
1263
|
+
name: z4.string().describe("Name of the dashboard"),
|
|
1264
|
+
oa3_spec: z4.string().describe(
|
|
1265
|
+
"Path or HTTP URL to OpenAPI 3.x specification file (supports OA2 and OA3)"
|
|
1266
|
+
),
|
|
1267
|
+
overrides: OverridesSchema.optional().describe(
|
|
1268
|
+
"Optional overrides for hosts, per-endpoint alarm thresholds, and query configurations"
|
|
1269
|
+
),
|
|
1270
|
+
queries: QueryConfigSchema.optional().describe(
|
|
1271
|
+
"Optional global query configuration overrides"
|
|
1272
|
+
),
|
|
1273
|
+
resource_type: z4.enum(["app-gateway", "api-management"]).optional().default("app-gateway").describe(
|
|
1274
|
+
"Type of Azure resource to monitor: app-gateway (Application Gateway) or api-management (API Management). Default: app-gateway"
|
|
1275
|
+
),
|
|
1276
|
+
response_time_threshold: z4.number().optional().default(DEFAULTS.response_time_threshold).describe("Default maximum response time in seconds. Default: 1.0"),
|
|
1277
|
+
terraform: TerraformConfigSchema.optional().describe(
|
|
1278
|
+
"Optional Terraform and environment-specific configuration"
|
|
1279
|
+
),
|
|
1280
|
+
timespan: z4.string().optional().default(DEFAULTS.timespan).describe(
|
|
1281
|
+
"Time range for dashboard queries (e.g., 5m, 1h, 24h). Default: 5m"
|
|
1282
|
+
)
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
// src/core/config/loader.ts
|
|
1286
|
+
import { readFile } from "fs/promises";
|
|
1287
|
+
import * as yaml from "js-yaml";
|
|
1288
|
+
async function loadConfig(configPath) {
|
|
1289
|
+
const content = await (async () => {
|
|
1290
|
+
try {
|
|
1291
|
+
if (configPath === "-") {
|
|
1292
|
+
return await readStdin();
|
|
1293
|
+
}
|
|
1294
|
+
return await readFile(configPath, "utf-8");
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
throw new FileError(
|
|
1297
|
+
`Failed to read config file: ${configPath} - ${error instanceof Error ? error.message : String(error)}`
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
})();
|
|
1301
|
+
try {
|
|
1302
|
+
const rawConfig = yaml.load(content);
|
|
1303
|
+
const config = ConfigSchema.parse(rawConfig);
|
|
1304
|
+
return config;
|
|
1305
|
+
} catch (error) {
|
|
1306
|
+
if (error && typeof error === "object" && "name" in error && error.name === "YAMLException") {
|
|
1307
|
+
throw new ConfigError(`Invalid YAML syntax: ${error.message}`);
|
|
1308
|
+
}
|
|
1309
|
+
throw new ConfigError(
|
|
1310
|
+
`Invalid configuration: ${error instanceof Error ? error.message : String(error)}`
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
async function readStdin() {
|
|
1315
|
+
const chunks = [];
|
|
1316
|
+
for await (const chunk of process.stdin) {
|
|
1317
|
+
chunks.push(chunk);
|
|
1318
|
+
}
|
|
1319
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// src/core/resolver/oa3-resolver.ts
|
|
1323
|
+
import SwaggerParser from "@apidevtools/swagger-parser";
|
|
1324
|
+
var OA3Resolver = class {
|
|
1325
|
+
specPath;
|
|
1326
|
+
constructor(specPath) {
|
|
1327
|
+
this.specPath = specPath;
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Resolve OpenAPI specification.
|
|
1331
|
+
* Parses fresh each time (no caching) to match Python behavior.
|
|
1332
|
+
* Resolves all $ref references automatically.
|
|
1333
|
+
*/
|
|
1334
|
+
async resolve() {
|
|
1335
|
+
try {
|
|
1336
|
+
const api = await SwaggerParser.dereference(this.specPath);
|
|
1337
|
+
return api;
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
if (error instanceof Error) {
|
|
1340
|
+
throw new ParseError(`OA3 parsing error: ${error.message}`);
|
|
1341
|
+
}
|
|
1342
|
+
throw new ParseError(`OA3 parsing error: ${String(error)}`);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
|
|
1347
|
+
// src/cli/helpers/output-writer.ts
|
|
1348
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
1349
|
+
async function ensureDirectory(dirPath) {
|
|
1350
|
+
try {
|
|
1351
|
+
await mkdir3(dirPath, { recursive: true });
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
throw new FileError(
|
|
1354
|
+
`Failed to create directory ${dirPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
function writeToStdout(content) {
|
|
1359
|
+
process.stdout.write(content);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// src/cli/helpers/spec-downloader.ts
|
|
1363
|
+
import { access, unlink, writeFile as writeFile3 } from "fs/promises";
|
|
1364
|
+
import tmp from "tmp";
|
|
1365
|
+
async function cleanupTempFile(filePath) {
|
|
1366
|
+
try {
|
|
1367
|
+
await access(filePath);
|
|
1368
|
+
await unlink(filePath);
|
|
1369
|
+
} catch {
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
async function downloadSpec(url) {
|
|
1373
|
+
const response = await fetch(url);
|
|
1374
|
+
if (!response.ok) {
|
|
1375
|
+
throw new Error(
|
|
1376
|
+
`Failed to download spec: ${response.status} ${response.statusText}`
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
1380
|
+
const tempFile = tmp.fileSync({
|
|
1381
|
+
postfix: ".yaml",
|
|
1382
|
+
prefix: "opex-spec-"
|
|
1383
|
+
}).name;
|
|
1384
|
+
try {
|
|
1385
|
+
await writeFile3(tempFile, Buffer.from(arrayBuffer));
|
|
1386
|
+
} catch (error) {
|
|
1387
|
+
throw new FileError(
|
|
1388
|
+
`Failed to write spec to ${tempFile}: ${error instanceof Error ? error.message : String(error)}`
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
return tempFile;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// src/cli/commands/generate.ts
|
|
1395
|
+
function createGenerateCommand() {
|
|
1396
|
+
const command = new Command("generate");
|
|
1397
|
+
command.description("Generate a dashboard definition from OpenAPI specification").requiredOption(
|
|
1398
|
+
"-t, --template-type <type>",
|
|
1399
|
+
"Type of template to generate",
|
|
1400
|
+
(value) => {
|
|
1401
|
+
if (!["azure-dashboard", "azure-dashboard-raw"].includes(value)) {
|
|
1402
|
+
throw new Error(
|
|
1403
|
+
"Invalid template type. Must be: azure-dashboard or azure-dashboard-raw"
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
return value;
|
|
1407
|
+
}
|
|
1408
|
+
).requiredOption(
|
|
1409
|
+
"-c, --config <path>",
|
|
1410
|
+
"Path to YAML configuration file (use - for stdin)"
|
|
1411
|
+
).option(
|
|
1412
|
+
"--package [path]",
|
|
1413
|
+
"Save template as a package in specified directory (default: current directory)",
|
|
1414
|
+
false
|
|
1415
|
+
).action(generateHandler);
|
|
1416
|
+
return command;
|
|
1417
|
+
}
|
|
1418
|
+
async function generateHandler(options) {
|
|
1419
|
+
try {
|
|
1420
|
+
const config = await loadConfig(options.config);
|
|
1421
|
+
const isHttp = config.oa3_spec.startsWith("http");
|
|
1422
|
+
const tempFile = isHttp ? await downloadSpec(config.oa3_spec) : void 0;
|
|
1423
|
+
const specPath = tempFile ?? config.oa3_spec;
|
|
1424
|
+
const allowedResourceTypes = ["app-gateway", "api-management"];
|
|
1425
|
+
if (!allowedResourceTypes.includes(config.resource_type)) {
|
|
1426
|
+
throw new ConfigError(
|
|
1427
|
+
`Invalid resource_type configuration: valid values are ${allowedResourceTypes.join(
|
|
1428
|
+
", "
|
|
1429
|
+
)}`
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
const resolver = new OA3Resolver(specPath);
|
|
1433
|
+
const builderParams = {
|
|
1434
|
+
action_groups_ids: config.action_groups,
|
|
1435
|
+
availability_threshold: config.availability_threshold,
|
|
1436
|
+
data_source_id: config.data_source,
|
|
1437
|
+
evaluation_frequency: config.evaluation_frequency,
|
|
1438
|
+
evaluation_time_window: config.evaluation_time_window,
|
|
1439
|
+
event_occurrences: config.event_occurrences,
|
|
1440
|
+
location: config.location,
|
|
1441
|
+
name: config.name,
|
|
1442
|
+
queries: config.queries || config.overrides?.queries,
|
|
1443
|
+
resolver,
|
|
1444
|
+
resource_type: config.resource_type,
|
|
1445
|
+
resources: [config.data_source],
|
|
1446
|
+
response_time_threshold: config.response_time_threshold,
|
|
1447
|
+
terraform: config.terraform,
|
|
1448
|
+
timespan: config.timespan
|
|
1449
|
+
};
|
|
1450
|
+
const builder = await createBuilder(options.templateType, builderParams);
|
|
1451
|
+
const overrides = config.overrides || {};
|
|
1452
|
+
if (options.package) {
|
|
1453
|
+
const outputPath = options.package;
|
|
1454
|
+
await ensureDirectory(outputPath);
|
|
1455
|
+
await builder.package(outputPath, overrides);
|
|
1456
|
+
} else {
|
|
1457
|
+
const output = builder.produce(overrides);
|
|
1458
|
+
writeToStdout(output);
|
|
1459
|
+
}
|
|
1460
|
+
if (tempFile) {
|
|
1461
|
+
await cleanupTempFile(tempFile);
|
|
1462
|
+
}
|
|
1463
|
+
} catch (error) {
|
|
1464
|
+
console.error(
|
|
1465
|
+
"Error:",
|
|
1466
|
+
error instanceof Error ? error.message : String(error)
|
|
1467
|
+
);
|
|
1468
|
+
process.exit(1);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// src/cli/index.ts
|
|
1473
|
+
var program = new Command2();
|
|
1474
|
+
program.name("opex_dashboard").description("Generate operational dashboards from OpenAPI 3 specifications");
|
|
1475
|
+
program.addCommand(createGenerateCommand()).version("0.1.0");
|
|
1476
|
+
program.parse(process.argv);
|