@pagopa/opex-dashboard 0.0.1 → 0.0.2

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/bin/index.js ADDED
@@ -0,0 +1,1451 @@
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?.environments) {
878
+ const envPromises = [];
879
+ for (const [env, envConfig] of Object.entries(
880
+ terraformConfig.environments
881
+ )) {
882
+ if (envConfig) {
883
+ const envPath = path2.join(outputPath, "env", env);
884
+ const envPromise = (async () => {
885
+ await mkdir(envPath, { recursive: true });
886
+ const backendTfvarsContent = generateBackendTfvars(
887
+ envConfig.backend
888
+ );
889
+ const terraformTfvarsContent = generateTerraformTfvars(envConfig);
890
+ await Promise.all([
891
+ writeFile(
892
+ path2.join(envPath, "backend.tfvars"),
893
+ backendTfvarsContent,
894
+ "utf-8"
895
+ ),
896
+ writeFile(
897
+ path2.join(envPath, "terraform.tfvars"),
898
+ terraformTfvarsContent,
899
+ "utf-8"
900
+ )
901
+ ]);
902
+ })();
903
+ envPromises.push(envPromise);
904
+ }
905
+ }
906
+ await Promise.all(envPromises);
907
+ }
908
+ } catch (error) {
909
+ throw new FileError(
910
+ `Failed to generate Terraform assets in ${outputPath}: ${error instanceof Error ? error.message : String(error)}`
911
+ );
912
+ }
913
+ }
914
+
915
+ // src/builders/azure-dashboard/template.ts
916
+ function azureDashboardTerraformTemplate(context) {
917
+ const name = context.name;
918
+ const dashboardProperties = context.dashboard_properties || "";
919
+ const basePath = context.base_path ?? "";
920
+ const actionGroupsJson = JSON.stringify(context.action_groups_ids).replace(
921
+ /,/g,
922
+ ", "
923
+ );
924
+ const dataSourceId = context.data_source_id;
925
+ const queryFns = context.resource_type === "api-management" ? api_management_exports : app_gateway_exports;
926
+ return `
927
+ locals {
928
+ name = "\${var.prefix}-\${var.env_short}-${name}"
929
+ dashboard_base_addr = "https://portal.azure.com/#@pagopait.onmicrosoft.com/dashboard/arm"
930
+ }
931
+
932
+ data "azurerm_resource_group" "this" {
933
+ name = "dashboards"
934
+ }
935
+
936
+ resource "azurerm_portal_dashboard" "this" {
937
+ name = local.name
938
+ resource_group_name = data.azurerm_resource_group.this.name
939
+ location = data.azurerm_resource_group.this.location
940
+
941
+ dashboard_properties = <<-PROPS
942
+ ${dashboardProperties}
943
+ PROPS
944
+
945
+ tags = var.tags
946
+ }
947
+
948
+
949
+ ${Object.entries(context.endpoints).map(([endpoint, props], i) => {
950
+ const fullPath = basePath + endpoint;
951
+ const availabilityQuery3 = queryFns.availabilityQuery({
952
+ ...context,
953
+ endpoint,
954
+ is_alarm: true,
955
+ threshold: props.availability_threshold,
956
+ ...props
957
+ });
958
+ const responseTimeQuery3 = queryFns.responseTimeQuery({
959
+ ...context,
960
+ endpoint,
961
+ is_alarm: true,
962
+ threshold: props.response_time_threshold,
963
+ ...props
964
+ });
965
+ return `resource "azurerm_monitor_scheduled_query_rules_alert" "alarm_availability_${i}" {
966
+ name = replace(join("_",split("/", "\${local.name}-availability @ ${fullPath}")), "/\\\\{|\\\\}/", "")
967
+ resource_group_name = data.azurerm_resource_group.this.name
968
+ location = data.azurerm_resource_group.this.location
969
+
970
+ action {
971
+ action_group = ${actionGroupsJson}
972
+ }
973
+
974
+ data_source_id = "${dataSourceId}"
975
+ description = "Availability for ${fullPath} is less than or equal to 99% - \${local.dashboard_base_addr}\${azurerm_portal_dashboard.this.id}"
976
+ enabled = true
977
+ auto_mitigation_enabled = false
978
+
979
+ query = <<-QUERY
980
+
981
+
982
+ ${availabilityQuery3}
983
+
984
+ QUERY
985
+
986
+ severity = 1
987
+ frequency = ${props.availability_evaluation_frequency ?? 10}
988
+ time_window = ${props.availability_evaluation_time_window ?? 20}
989
+ trigger {
990
+ operator = "GreaterThanOrEqual"
991
+ threshold = ${props.availability_event_occurrences ?? 1}
992
+ }
993
+
994
+ tags = var.tags
995
+ }
996
+
997
+ resource "azurerm_monitor_scheduled_query_rules_alert" "alarm_time_${i}" {
998
+ name = replace(join("_",split("/", "\${local.name}-responsetime @ ${fullPath}")), "/\\\\{|\\\\}/", "")
999
+ resource_group_name = data.azurerm_resource_group.this.name
1000
+ location = data.azurerm_resource_group.this.location
1001
+
1002
+ action {
1003
+ action_group = ${actionGroupsJson}
1004
+ }
1005
+
1006
+ data_source_id = "${dataSourceId}"
1007
+ description = "Response time for ${fullPath} is less than or equal to 1s - \${local.dashboard_base_addr}\${azurerm_portal_dashboard.this.id}"
1008
+ enabled = true
1009
+ auto_mitigation_enabled = false
1010
+
1011
+ query = <<-QUERY
1012
+
1013
+
1014
+ ${responseTimeQuery3}
1015
+
1016
+ QUERY
1017
+
1018
+ severity = 1
1019
+ frequency = ${props.response_time_evaluation_frequency ?? 10}
1020
+ time_window = ${props.response_time_evaluation_time_window ?? 20}
1021
+ trigger {
1022
+ operator = "GreaterThanOrEqual"
1023
+ threshold = ${props.response_time_event_occurrences ?? 1}
1024
+ }
1025
+
1026
+ tags = var.tags
1027
+ }
1028
+ `;
1029
+ }).join("\n")}
1030
+ `;
1031
+ }
1032
+
1033
+ // src/builders/azure-dashboard/builder.ts
1034
+ var AzDashboardBuilder = class extends Builder {
1035
+ rawBuilder;
1036
+ terraformConfig;
1037
+ constructor(options) {
1038
+ super(azureDashboardTerraformTemplate, {
1039
+ action_groups_ids: options.actionGroupsIds,
1040
+ data_source_id: options.dataSourceId,
1041
+ endpoints: {},
1042
+ evaluation_frequency: options.evaluationFrequency,
1043
+ event_occurrences: options.eventOccurrences,
1044
+ hosts: [],
1045
+ location: options.location,
1046
+ name: options.name.replace(/ /g, "_"),
1047
+ // Replace spaces with underscores for Terraform compatibility
1048
+ resource_type: options.resourceType,
1049
+ time_window: options.evaluationTimeWindow,
1050
+ timespan: options.timespan
1051
+ });
1052
+ this.rawBuilder = options.dashboardBuilder;
1053
+ this.terraformConfig = options.terraformConfig;
1054
+ }
1055
+ /**
1056
+ * Package Terraform configuration.
1057
+ * Creates opex.tf and generates terraform assets (main.tf, variables.tf, env/).
1058
+ */
1059
+ async package(outputPath, values = {}) {
1060
+ try {
1061
+ await mkdir2(outputPath, { recursive: true });
1062
+ const terraformFilePath = path3.join(outputPath, "opex.tf");
1063
+ const content = this.produce(values);
1064
+ await writeFile2(terraformFilePath, content, "utf-8");
1065
+ await generateTerraformAssets(outputPath, this.terraformConfig);
1066
+ } catch (error) {
1067
+ throw new FileError(
1068
+ `Failed to package Terraform configuration in ${outputPath}: ${error instanceof Error ? error.message : String(error)}`
1069
+ );
1070
+ }
1071
+ }
1072
+ /**
1073
+ * Render Terraform template with embedded dashboard JSON from raw builder.
1074
+ */
1075
+ produce(values = {}) {
1076
+ const normalizedValues = values.endpoints ? { ...values, endpoints: normalizeEndpointKeys(values.endpoints) } : values;
1077
+ const rawJson = this.rawBuilder.produce(normalizedValues);
1078
+ const dashboard = JSON.parse(rawJson);
1079
+ this.properties.dashboard_properties = JSON.stringify(
1080
+ dashboard.properties,
1081
+ null,
1082
+ 2
1083
+ );
1084
+ const rawProps = this.rawBuilder.props();
1085
+ this.properties.hosts = rawProps.hosts;
1086
+ this.properties.endpoints = rawProps.endpoints;
1087
+ return super.produce(normalizedValues);
1088
+ }
1089
+ };
1090
+
1091
+ // src/core/builder-factory.ts
1092
+ async function createAzureRawBuilder(params) {
1093
+ const oa3Spec = await params.resolver.resolve();
1094
+ return new AzDashboardRawBuilder({
1095
+ availabilityThreshold: params.availability_threshold,
1096
+ evaluationFrequency: params.evaluation_frequency,
1097
+ evaluationTimeWindow: params.evaluation_time_window,
1098
+ eventOccurrences: params.event_occurrences,
1099
+ location: params.location,
1100
+ name: params.name,
1101
+ oa3Spec,
1102
+ queries: params.queries,
1103
+ resources: params.resources,
1104
+ resourceType: params.resource_type,
1105
+ responseTimeThreshold: params.response_time_threshold,
1106
+ timespan: params.timespan
1107
+ });
1108
+ }
1109
+ async function createAzureTerraformBuilder(params) {
1110
+ const rawBuilder = await createAzureRawBuilder(params);
1111
+ return new AzDashboardBuilder({
1112
+ actionGroupsIds: params.action_groups_ids,
1113
+ dashboardBuilder: rawBuilder,
1114
+ dataSourceId: params.data_source_id,
1115
+ evaluationFrequency: params.evaluation_frequency,
1116
+ evaluationTimeWindow: params.evaluation_time_window,
1117
+ eventOccurrences: params.event_occurrences,
1118
+ location: params.location,
1119
+ name: params.name,
1120
+ resourceType: params.resource_type,
1121
+ terraformConfig: params.terraform,
1122
+ timespan: params.timespan
1123
+ });
1124
+ }
1125
+ var builderRegistry = {
1126
+ "azure-dashboard": createAzureTerraformBuilder,
1127
+ "azure-dashboard-raw": createAzureRawBuilder
1128
+ };
1129
+ async function createBuilder(templateType, params) {
1130
+ const factory = builderRegistry[templateType];
1131
+ if (!factory) {
1132
+ throw new InvalidBuilderError(
1133
+ `Invalid builder error: unknown builder ${templateType}`
1134
+ );
1135
+ }
1136
+ try {
1137
+ return await factory(params);
1138
+ } catch (error) {
1139
+ throw new InvalidBuilderError(
1140
+ `Failed to create builder: ${error instanceof Error ? error.message : String(error)}`
1141
+ );
1142
+ }
1143
+ }
1144
+
1145
+ // src/core/config/config.schema.ts
1146
+ import { z as z4 } from "zod";
1147
+
1148
+ // src/core/shared/query-config.schema.ts
1149
+ import { z as z3 } from "zod";
1150
+ var QueryConfigSchema = z3.object({
1151
+ response_time_percentile: z3.number().default(95).describe("Percentile for response time queries. Default: 95"),
1152
+ status_code_categories: z3.array(z3.string()).default(["1XX", "2XX", "3XX", "4XX", "5XX"]).describe("HTTP status code categories for response codes queries")
1153
+ });
1154
+
1155
+ // src/core/config/defaults.ts
1156
+ var DEFAULT_TIMESPAN = "5m";
1157
+ var DEFAULTS = {
1158
+ availability_threshold: DEFAULT_AVAILABILITY_THRESHOLD,
1159
+ evaluation_frequency: EVALUATION_FREQUENCY_MINUTES,
1160
+ evaluation_time_window: TIME_WINDOW_MINUTES,
1161
+ event_occurrences: EVENT_OCCURRENCES,
1162
+ response_time_threshold: DEFAULT_RESPONSE_TIME_THRESHOLD,
1163
+ timespan: DEFAULT_TIMESPAN
1164
+ };
1165
+
1166
+ // src/core/config/config.schema.ts
1167
+ var EndpointOverrideSchema = EndpointOverridePropertiesSchema.extend({
1168
+ availability_evaluation_frequency: z4.number().optional().describe(
1169
+ "Frequency in minutes to evaluate availability alarm. Default: 10"
1170
+ ),
1171
+ availability_evaluation_time_window: z4.number().optional().describe(
1172
+ "Time window in minutes for availability alarm evaluation. Default: 20"
1173
+ ),
1174
+ availability_event_occurrences: z4.number().optional().describe(
1175
+ "Number of event occurrences to trigger availability alarm. Default: 1"
1176
+ ),
1177
+ availability_threshold: z4.number().optional().describe("Minimum availability percentage (0-1). Default: 0.99 (99%)"),
1178
+ response_time_evaluation_frequency: z4.number().optional().describe(
1179
+ "Frequency in minutes to evaluate response time alarm. Default: 10"
1180
+ ),
1181
+ response_time_evaluation_time_window: z4.number().optional().describe(
1182
+ "Time window in minutes for response time alarm evaluation. Default: 20"
1183
+ ),
1184
+ response_time_event_occurrences: z4.number().optional().describe(
1185
+ "Number of event occurrences to trigger response time alarm. Default: 1"
1186
+ ),
1187
+ response_time_threshold: z4.number().optional().describe("Maximum response time in seconds. Default: 1")
1188
+ });
1189
+ var OverridesSchema = z4.object({
1190
+ endpoints: z4.record(z4.string(), EndpointOverrideSchema).optional().describe(
1191
+ "Override alarm thresholds and settings for specific endpoints (key: endpoint path)"
1192
+ ),
1193
+ hosts: z4.array(z4.string()).optional().describe(
1194
+ "Override host URLs from OpenAPI spec (e.g., https://example.com)"
1195
+ ),
1196
+ queries: QueryConfigSchema.optional().describe(
1197
+ "Optional query configuration overrides"
1198
+ )
1199
+ });
1200
+ var BackendConfigSchema = z4.object({
1201
+ container_name: z4.string().describe("Blob container name for Terraform state"),
1202
+ key: z4.string().describe("State file key/path"),
1203
+ resource_group_name: z4.string().describe("Azure resource group for backend state"),
1204
+ storage_account_name: z4.string().describe("Storage account for Terraform state")
1205
+ });
1206
+ var EnvironmentConfigSchema = z4.object({
1207
+ backend: BackendConfigSchema.optional().describe(
1208
+ "Azure backend configuration for Terraform state"
1209
+ ),
1210
+ env_short: z4.string().max(1).describe("Environment short name (1 char: 'd'=dev, 'u'=uat, 'p'=prod)"),
1211
+ prefix: z4.string().max(6).describe("Project prefix (max 6 chars, e.g., 'io', 'pagopa')")
1212
+ });
1213
+ var TerraformConfigSchema = z4.object({
1214
+ environments: z4.object({
1215
+ dev: EnvironmentConfigSchema.optional(),
1216
+ prod: EnvironmentConfigSchema.optional(),
1217
+ uat: EnvironmentConfigSchema.optional()
1218
+ }).optional().describe("Environment-specific configurations for dev/uat/prod")
1219
+ });
1220
+ var ConfigSchema = z4.object({
1221
+ action_groups: z4.array(z4.string()).describe(
1222
+ "Array of Azure Action Group resource IDs for alarm notifications"
1223
+ ),
1224
+ availability_threshold: z4.number().optional().default(DEFAULTS.availability_threshold).describe(
1225
+ "Default minimum availability percentage (0-1). Default: 0.99 (99%)"
1226
+ ),
1227
+ data_source: z4.string().describe(
1228
+ "Azure resource ID for metrics data source (Application Gateway or API Management)"
1229
+ ),
1230
+ evaluation_frequency: z4.number().optional().default(DEFAULTS.evaluation_frequency).describe("Default frequency in minutes to evaluate alarms. Default: 10"),
1231
+ evaluation_time_window: z4.number().optional().default(DEFAULTS.evaluation_time_window).describe(
1232
+ "Default time window in minutes for alarm evaluation. Default: 20"
1233
+ ),
1234
+ event_occurrences: z4.number().optional().default(DEFAULTS.event_occurrences).describe(
1235
+ "Default number of event occurrences to trigger an alarm. Default: 1"
1236
+ ),
1237
+ location: z4.string().describe("Azure region/location for the dashboard (e.g., West Europe)"),
1238
+ name: z4.string().describe("Name of the dashboard"),
1239
+ oa3_spec: z4.string().describe(
1240
+ "Path or HTTP URL to OpenAPI 3.x specification file (supports OA2 and OA3)"
1241
+ ),
1242
+ overrides: OverridesSchema.optional().describe(
1243
+ "Optional overrides for hosts, per-endpoint alarm thresholds, and query configurations"
1244
+ ),
1245
+ queries: QueryConfigSchema.optional().describe(
1246
+ "Optional global query configuration overrides"
1247
+ ),
1248
+ resource_type: z4.enum(["app-gateway", "api-management"]).optional().default("app-gateway").describe(
1249
+ "Type of Azure resource to monitor: app-gateway (Application Gateway) or api-management (API Management). Default: app-gateway"
1250
+ ),
1251
+ response_time_threshold: z4.number().optional().default(DEFAULTS.response_time_threshold).describe("Default maximum response time in seconds. Default: 1.0"),
1252
+ terraform: TerraformConfigSchema.optional().describe(
1253
+ "Optional Terraform and environment-specific configuration"
1254
+ ),
1255
+ timespan: z4.string().optional().default(DEFAULTS.timespan).describe(
1256
+ "Time range for dashboard queries (e.g., 5m, 1h, 24h). Default: 5m"
1257
+ )
1258
+ });
1259
+
1260
+ // src/core/config/loader.ts
1261
+ import { readFile } from "fs/promises";
1262
+ import * as yaml from "js-yaml";
1263
+ async function loadConfig(configPath) {
1264
+ const content = await (async () => {
1265
+ try {
1266
+ if (configPath === "-") {
1267
+ return await readStdin();
1268
+ }
1269
+ return await readFile(configPath, "utf-8");
1270
+ } catch (error) {
1271
+ throw new FileError(
1272
+ `Failed to read config file: ${configPath} - ${error instanceof Error ? error.message : String(error)}`
1273
+ );
1274
+ }
1275
+ })();
1276
+ try {
1277
+ const rawConfig = yaml.load(content);
1278
+ const config = ConfigSchema.parse(rawConfig);
1279
+ return config;
1280
+ } catch (error) {
1281
+ if (error && typeof error === "object" && "name" in error && error.name === "YAMLException") {
1282
+ throw new ConfigError(`Invalid YAML syntax: ${error.message}`);
1283
+ }
1284
+ throw new ConfigError(
1285
+ `Invalid configuration: ${error instanceof Error ? error.message : String(error)}`
1286
+ );
1287
+ }
1288
+ }
1289
+ async function readStdin() {
1290
+ const chunks = [];
1291
+ for await (const chunk of process.stdin) {
1292
+ chunks.push(chunk);
1293
+ }
1294
+ return Buffer.concat(chunks).toString("utf-8");
1295
+ }
1296
+
1297
+ // src/core/resolver/oa3-resolver.ts
1298
+ import SwaggerParser from "@apidevtools/swagger-parser";
1299
+ var OA3Resolver = class {
1300
+ specPath;
1301
+ constructor(specPath) {
1302
+ this.specPath = specPath;
1303
+ }
1304
+ /**
1305
+ * Resolve OpenAPI specification.
1306
+ * Parses fresh each time (no caching) to match Python behavior.
1307
+ * Resolves all $ref references automatically.
1308
+ */
1309
+ async resolve() {
1310
+ try {
1311
+ const api = await SwaggerParser.dereference(this.specPath);
1312
+ return api;
1313
+ } catch (error) {
1314
+ if (error instanceof Error) {
1315
+ throw new ParseError(`OA3 parsing error: ${error.message}`);
1316
+ }
1317
+ throw new ParseError(`OA3 parsing error: ${String(error)}`);
1318
+ }
1319
+ }
1320
+ };
1321
+
1322
+ // src/cli/helpers/output-writer.ts
1323
+ import { mkdir as mkdir3 } from "fs/promises";
1324
+ async function ensureDirectory(dirPath) {
1325
+ try {
1326
+ await mkdir3(dirPath, { recursive: true });
1327
+ } catch (error) {
1328
+ throw new FileError(
1329
+ `Failed to create directory ${dirPath}: ${error instanceof Error ? error.message : String(error)}`
1330
+ );
1331
+ }
1332
+ }
1333
+ function writeToStdout(content) {
1334
+ process.stdout.write(content);
1335
+ }
1336
+
1337
+ // src/cli/helpers/spec-downloader.ts
1338
+ import { access, unlink, writeFile as writeFile3 } from "fs/promises";
1339
+ import tmp from "tmp";
1340
+ async function cleanupTempFile(filePath) {
1341
+ try {
1342
+ await access(filePath);
1343
+ await unlink(filePath);
1344
+ } catch {
1345
+ }
1346
+ }
1347
+ async function downloadSpec(url) {
1348
+ const response = await fetch(url);
1349
+ if (!response.ok) {
1350
+ throw new Error(
1351
+ `Failed to download spec: ${response.status} ${response.statusText}`
1352
+ );
1353
+ }
1354
+ const arrayBuffer = await response.arrayBuffer();
1355
+ const tempFile = tmp.fileSync({
1356
+ postfix: ".yaml",
1357
+ prefix: "opex-spec-"
1358
+ }).name;
1359
+ try {
1360
+ await writeFile3(tempFile, Buffer.from(arrayBuffer));
1361
+ } catch (error) {
1362
+ throw new FileError(
1363
+ `Failed to write spec to ${tempFile}: ${error instanceof Error ? error.message : String(error)}`
1364
+ );
1365
+ }
1366
+ return tempFile;
1367
+ }
1368
+
1369
+ // src/cli/commands/generate.ts
1370
+ function createGenerateCommand() {
1371
+ const command = new Command("generate");
1372
+ command.description("Generate a dashboard definition from OpenAPI specification").requiredOption(
1373
+ "-t, --template-type <type>",
1374
+ "Type of template to generate",
1375
+ (value) => {
1376
+ if (!["azure-dashboard", "azure-dashboard-raw"].includes(value)) {
1377
+ throw new Error(
1378
+ "Invalid template type. Must be: azure-dashboard or azure-dashboard-raw"
1379
+ );
1380
+ }
1381
+ return value;
1382
+ }
1383
+ ).requiredOption(
1384
+ "-c, --config <path>",
1385
+ "Path to YAML configuration file (use - for stdin)"
1386
+ ).option(
1387
+ "--package [path]",
1388
+ "Save template as a package in specified directory (default: current directory)",
1389
+ false
1390
+ ).action(generateHandler);
1391
+ return command;
1392
+ }
1393
+ async function generateHandler(options) {
1394
+ try {
1395
+ const config = await loadConfig(options.config);
1396
+ const isHttp = config.oa3_spec.startsWith("http");
1397
+ const tempFile = isHttp ? await downloadSpec(config.oa3_spec) : void 0;
1398
+ const specPath = tempFile ?? config.oa3_spec;
1399
+ const allowedResourceTypes = ["app-gateway", "api-management"];
1400
+ if (!allowedResourceTypes.includes(config.resource_type)) {
1401
+ throw new ConfigError(
1402
+ `Invalid resource_type configuration: valid values are ${allowedResourceTypes.join(
1403
+ ", "
1404
+ )}`
1405
+ );
1406
+ }
1407
+ const resolver = new OA3Resolver(specPath);
1408
+ const builderParams = {
1409
+ action_groups_ids: config.action_groups,
1410
+ availability_threshold: config.availability_threshold,
1411
+ data_source_id: config.data_source,
1412
+ evaluation_frequency: config.evaluation_frequency,
1413
+ evaluation_time_window: config.evaluation_time_window,
1414
+ event_occurrences: config.event_occurrences,
1415
+ location: config.location,
1416
+ name: config.name,
1417
+ queries: config.queries || config.overrides?.queries,
1418
+ resolver,
1419
+ resource_type: config.resource_type,
1420
+ resources: [config.data_source],
1421
+ response_time_threshold: config.response_time_threshold,
1422
+ terraform: config.terraform,
1423
+ timespan: config.timespan
1424
+ };
1425
+ const builder = await createBuilder(options.templateType, builderParams);
1426
+ const overrides = config.overrides || {};
1427
+ if (options.package) {
1428
+ const outputPath = options.package;
1429
+ await ensureDirectory(outputPath);
1430
+ await builder.package(outputPath, overrides);
1431
+ } else {
1432
+ const output = builder.produce(overrides);
1433
+ writeToStdout(output);
1434
+ }
1435
+ if (tempFile) {
1436
+ await cleanupTempFile(tempFile);
1437
+ }
1438
+ } catch (error) {
1439
+ console.error(
1440
+ "Error:",
1441
+ error instanceof Error ? error.message : String(error)
1442
+ );
1443
+ process.exit(1);
1444
+ }
1445
+ }
1446
+
1447
+ // src/cli/index.ts
1448
+ var program = new Command2();
1449
+ program.name("opex_dashboard").description("Generate operational dashboards from OpenAPI 3 specifications");
1450
+ program.addCommand(createGenerateCommand()).version("0.0.2");
1451
+ program.parse(process.argv);