@axiom-lattice/gateway 2.1.28 → 2.1.30

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.
@@ -0,0 +1,989 @@
1
+ import { FastifyRequest, FastifyReply } from "fastify";
2
+ import {
3
+ getStoreLattice,
4
+ metricsServerManager,
5
+ SemanticMetricsClient,
6
+ } from "@axiom-lattice/core";
7
+ import type {
8
+ MetricsServerConfigStore,
9
+ MetricsServerConfigEntry,
10
+ CreateMetricsServerConfigRequest,
11
+ UpdateMetricsServerConfigRequest,
12
+ MetricsServerConfig,
13
+ DataSource,
14
+ SemanticMetricsQueryRequest,
15
+ SemanticMetricsServerConfig,
16
+ } from "@axiom-lattice/protocols";
17
+ import { randomUUID } from "crypto";
18
+
19
+ /**
20
+ * Metrics Server Config Controller
21
+ * Handles metrics server configuration CRUD operations
22
+ */
23
+
24
+ /**
25
+ * Get tenant ID from request headers
26
+ */
27
+ function getTenantId(request: FastifyRequest): string {
28
+ return (request.headers["x-tenant-id"] as string) || "default";
29
+ }
30
+
31
+ /**
32
+ * Metrics server config list response
33
+ */
34
+ interface MetricsServerConfigListResponse {
35
+ success: boolean;
36
+ message: string;
37
+ data: {
38
+ records: MetricsServerConfigEntry[];
39
+ total: number;
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Metrics server config response
45
+ */
46
+ interface MetricsServerConfigResponse {
47
+ success: boolean;
48
+ message: string;
49
+ data?: MetricsServerConfigEntry;
50
+ }
51
+
52
+ /**
53
+ * Test connection response
54
+ */
55
+ interface TestConnectionResponse {
56
+ success: boolean;
57
+ message: string;
58
+ data?: {
59
+ connected: boolean;
60
+ latency?: number;
61
+ error?: string;
62
+ };
63
+ }
64
+
65
+ /**
66
+ * List available metrics response
67
+ */
68
+ interface ListMetricsResponse {
69
+ success: boolean;
70
+ message: string;
71
+ data?: {
72
+ metrics: Array<{
73
+ name: string;
74
+ type?: string;
75
+ description?: string;
76
+ }>;
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Query metric data response
82
+ */
83
+ interface QueryMetricDataResponse {
84
+ success: boolean;
85
+ message: string;
86
+ data?: {
87
+ metricName: string;
88
+ dataPoints: Array<{
89
+ timestamp: number;
90
+ value: number;
91
+ labels?: Record<string, string>;
92
+ }>;
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Data sources list response
98
+ */
99
+ interface DataSourcesResponse {
100
+ success: boolean;
101
+ message: string;
102
+ data?: {
103
+ datasources: DataSource[];
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Datasource metrics response
109
+ */
110
+ interface DatasourceMetricsResponse {
111
+ success: boolean;
112
+ message: string;
113
+ data?: {
114
+ index: {
115
+ datasourceId: number;
116
+ datasourceName: string;
117
+ catalogVersion: string;
118
+ domainCategories: string[];
119
+ metrics: Array<{
120
+ metricName: string;
121
+ displayName: string;
122
+ domain: string;
123
+ shortDesc: string;
124
+ searchKeywords: string[];
125
+ registered: boolean;
126
+ }>;
127
+ };
128
+ details: Array<{
129
+ datasourceId: number;
130
+ metricName: string;
131
+ displayName: string;
132
+ domain: string;
133
+ description: string;
134
+ dataType: string;
135
+ format: string;
136
+ defaultTimeContext: {
137
+ timeDimension: string;
138
+ label: string;
139
+ granularity: string;
140
+ window: string;
141
+ supportedGrains: string[];
142
+ };
143
+ supportedDimensions: Array<{
144
+ dim_id: string;
145
+ field_name: string;
146
+ type: string;
147
+ filter_operators?: string[];
148
+ filter_example?: object;
149
+ group_by_example?: string;
150
+ }>;
151
+ aiAgentContext: {
152
+ polarity: string;
153
+ synonyms: string[];
154
+ thresholds: Array<{
155
+ metric: string;
156
+ operator: string;
157
+ value: number;
158
+ level: string;
159
+ }>;
160
+ diagnosticWorkflow: {
161
+ trigger: {
162
+ any_of: Array<{
163
+ metric: string;
164
+ operator: string;
165
+ value: number;
166
+ }>;
167
+ };
168
+ actions: Array<{
169
+ type: string;
170
+ metric?: string;
171
+ dimensions?: string[];
172
+ intent: string;
173
+ }>;
174
+ analysis_logic: string;
175
+ };
176
+ humanReadableExplanation: string;
177
+ };
178
+ }>;
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Semantic query response
184
+ */
185
+ interface SemanticQueryResponse {
186
+ success: boolean;
187
+ message: string;
188
+ data?: {
189
+ datasourceId: string | number;
190
+ metrics: string[];
191
+ dataPoints: Array<{
192
+ timestamp?: number;
193
+ value: number;
194
+ metricName?: string;
195
+ labels?: Record<string, string>;
196
+ }>;
197
+ metadata?: {
198
+ rowCount?: number;
199
+ queryTimeMs?: number;
200
+ };
201
+ };
202
+ }
203
+
204
+ /**
205
+ * Get all metrics server configs for a tenant
206
+ */
207
+ export async function getMetricsServerConfigList(
208
+ request: FastifyRequest,
209
+ reply: FastifyReply
210
+ ): Promise<MetricsServerConfigListResponse> {
211
+ const tenantId = getTenantId(request);
212
+
213
+ try {
214
+ const storeLattice = getStoreLattice("default", "metrics");
215
+ const store: MetricsServerConfigStore = storeLattice.store;
216
+
217
+ const configs = await store.getAllConfigs(tenantId);
218
+
219
+ return {
220
+ success: true,
221
+ message: "Metrics server configurations retrieved successfully",
222
+ data: {
223
+ records: configs,
224
+ total: configs.length,
225
+ },
226
+ };
227
+ } catch (error) {
228
+ console.error("Failed to get metrics server configs:", error);
229
+ return {
230
+ success: false,
231
+ message: "Failed to retrieve metrics server configurations",
232
+ data: {
233
+ records: [],
234
+ total: 0,
235
+ },
236
+ };
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Get metrics server config by key
242
+ */
243
+ export async function getMetricsServerConfig(
244
+ request: FastifyRequest,
245
+ reply: FastifyReply
246
+ ): Promise<MetricsServerConfigResponse> {
247
+ const tenantId = getTenantId(request);
248
+ const { key } = request.params as { key: string };
249
+
250
+ try {
251
+ const storeLattice = getStoreLattice("default", "metrics");
252
+ const store: MetricsServerConfigStore = storeLattice.store;
253
+
254
+ const config = await store.getConfigByKey(tenantId, key);
255
+
256
+ if (!config) {
257
+ return {
258
+ success: false,
259
+ message: "Metrics server configuration not found",
260
+ };
261
+ }
262
+
263
+ return {
264
+ success: true,
265
+ message: "Metrics server configuration retrieved successfully",
266
+ data: config,
267
+ };
268
+ } catch (error) {
269
+ console.error("Failed to get metrics server config:", error);
270
+ return {
271
+ success: false,
272
+ message: "Failed to retrieve metrics server configuration",
273
+ };
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Create new metrics server config
279
+ */
280
+ export async function createMetricsServerConfig(
281
+ request: FastifyRequest,
282
+ reply: FastifyReply
283
+ ): Promise<MetricsServerConfigResponse> {
284
+ const tenantId = getTenantId(request);
285
+ const body = request.body as CreateMetricsServerConfigRequest & {
286
+ id?: string;
287
+ selectedDataSources?: string[];
288
+ };
289
+
290
+ try {
291
+ const storeLattice = getStoreLattice("default", "metrics");
292
+ const store: MetricsServerConfigStore = storeLattice.store;
293
+
294
+ // Check if key already exists
295
+ const existing = await store.getConfigByKey(tenantId, body.key);
296
+ if (existing) {
297
+ reply.code(409);
298
+ return {
299
+ success: false,
300
+ message: "Metrics server configuration with this key already exists",
301
+ };
302
+ }
303
+
304
+ if (body.config.type === "semantic" && !body.selectedDataSources) {
305
+ reply.code(400);
306
+ return {
307
+ success: false,
308
+ message: "selectedDataSources is required for semantic metrics servers",
309
+ };
310
+ }
311
+
312
+ const id = body.id || randomUUID();
313
+ const configData: CreateMetricsServerConfigRequest = {
314
+ key: body.key,
315
+ name: body.name,
316
+ description: body.description,
317
+ config: body.config.type === "semantic"
318
+ ? {
319
+ ...body.config,
320
+ selectedDataSources: body.selectedDataSources || [],
321
+ } as SemanticMetricsServerConfig
322
+ : body.config,
323
+ };
324
+
325
+ const config = await store.createConfig(tenantId, id, configData);
326
+
327
+ // Auto-register to MetricsServerManager
328
+ try {
329
+ metricsServerManager.registerServer(config.key, config.config);
330
+ } catch (error) {
331
+ console.warn("Failed to auto-register metrics server:", error);
332
+ }
333
+
334
+ reply.code(201);
335
+ return {
336
+ success: true,
337
+ message: "Metrics server configuration created successfully",
338
+ data: config,
339
+ };
340
+ } catch (error) {
341
+ console.error("Failed to create metrics server config:", error);
342
+ return {
343
+ success: false,
344
+ message: "Failed to create metrics server configuration",
345
+ };
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Update metrics server config
351
+ */
352
+ export async function updateMetricsServerConfig(
353
+ request: FastifyRequest,
354
+ reply: FastifyReply
355
+ ): Promise<MetricsServerConfigResponse> {
356
+ const tenantId = getTenantId(request);
357
+ const { key } = request.params as { key: string };
358
+ const updates = request.body as Partial<UpdateMetricsServerConfigRequest> & {
359
+ selectedDataSources?: string[];
360
+ };
361
+
362
+ try {
363
+ const storeLattice = getStoreLattice("default", "metrics");
364
+ const store: MetricsServerConfigStore = storeLattice.store;
365
+
366
+ const existing = await store.getConfigByKey(tenantId, key);
367
+ if (!existing) {
368
+ reply.code(404);
369
+ return {
370
+ success: false,
371
+ message: "Metrics server configuration not found",
372
+ };
373
+ }
374
+
375
+ const isSemantic = updates.config?.type === "semantic" ||
376
+ (existing.config.type === "semantic" && !updates.config?.type);
377
+
378
+ if (isSemantic && updates.selectedDataSources !== undefined) {
379
+ updates.config = {
380
+ ...existing.config,
381
+ ...updates.config,
382
+ type: "semantic",
383
+ selectedDataSources: updates.selectedDataSources,
384
+ } as SemanticMetricsServerConfig;
385
+ }
386
+
387
+ const updated = await store.updateConfig(tenantId, existing.id, updates);
388
+
389
+ if (!updated) {
390
+ return {
391
+ success: false,
392
+ message: "Failed to update metrics server configuration",
393
+ };
394
+ }
395
+
396
+ if (updates.config) {
397
+ try {
398
+ metricsServerManager.registerServer(updated.key, updated.config);
399
+ } catch (error) {
400
+ console.warn("Failed to re-register metrics server:", error);
401
+ }
402
+ }
403
+
404
+ return {
405
+ success: true,
406
+ message: "Metrics server configuration updated successfully",
407
+ data: updated,
408
+ };
409
+ } catch (error) {
410
+ console.error("Failed to update metrics server config:", error);
411
+ return {
412
+ success: false,
413
+ message: "Failed to update metrics server configuration",
414
+ };
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Delete metrics server config by key or id
420
+ */
421
+ export async function deleteMetricsServerConfig(
422
+ request: FastifyRequest,
423
+ reply: FastifyReply
424
+ ): Promise<MetricsServerConfigResponse> {
425
+ const tenantId = getTenantId(request);
426
+ const { keyOrId } = request.params as { keyOrId: string };
427
+
428
+ try {
429
+ const storeLattice = getStoreLattice("default", "metrics");
430
+ const store: MetricsServerConfigStore = storeLattice.store;
431
+
432
+ // Try to find by key first, then by id
433
+ let config = await store.getConfigByKey(tenantId, keyOrId);
434
+ let configKey = keyOrId;
435
+
436
+ if (!config) {
437
+ // Try to find by id
438
+ config = await store.getConfigById(tenantId, keyOrId);
439
+ if (config) {
440
+ configKey = config.key;
441
+ }
442
+ }
443
+
444
+ if (!config) {
445
+ reply.code(404);
446
+ return {
447
+ success: false,
448
+ message: "Metrics server configuration not found",
449
+ };
450
+ }
451
+
452
+ const deleted = await store.deleteConfig(tenantId, config.id);
453
+
454
+ if (!deleted) {
455
+ return {
456
+ success: false,
457
+ message: "Failed to delete metrics server configuration",
458
+ };
459
+ }
460
+
461
+ // Remove from MetricsServerManager
462
+ try {
463
+ if (metricsServerManager.hasServer(configKey)) {
464
+ metricsServerManager.removeServer(configKey);
465
+ }
466
+ } catch (error) {
467
+ console.warn("Failed to remove from MetricsServerManager:", error);
468
+ }
469
+
470
+ return {
471
+ success: true,
472
+ message: "Metrics server configuration deleted successfully",
473
+ };
474
+ } catch (error) {
475
+ console.error("Failed to delete metrics server config:", error);
476
+ return {
477
+ success: false,
478
+ message: "Failed to delete metrics server configuration",
479
+ };
480
+ }
481
+ }
482
+
483
+ /**
484
+ * Test metrics server connection
485
+ */
486
+ export async function testMetricsServerConnection(
487
+ request: FastifyRequest,
488
+ reply: FastifyReply
489
+ ): Promise<TestConnectionResponse> {
490
+ const tenantId = getTenantId(request);
491
+ const { key } = request.params as { key: string };
492
+
493
+ try {
494
+ const storeLattice = getStoreLattice("default", "metrics");
495
+ const store: MetricsServerConfigStore = storeLattice.store;
496
+
497
+ const config = await store.getConfigByKey(tenantId, key);
498
+ if (!config) {
499
+ reply.code(404);
500
+ return {
501
+ success: false,
502
+ message: "Metrics server configuration not found",
503
+ };
504
+ }
505
+
506
+ // Register temporarily for testing
507
+ const testKey = `__test_${key}_${Date.now()}`;
508
+ metricsServerManager.registerServer(testKey, config.config);
509
+
510
+ try {
511
+ const client = metricsServerManager.getClient(testKey);
512
+ const result = await client.testConnection();
513
+
514
+ // Cleanup
515
+ metricsServerManager.removeServer(testKey);
516
+
517
+ return {
518
+ success: true,
519
+ message: result.connected ? "Connection test successful" : "Connection test failed",
520
+ data: result,
521
+ };
522
+ } catch (error) {
523
+ // Cleanup on error
524
+ try {
525
+ metricsServerManager.removeServer(testKey);
526
+ } catch {}
527
+
528
+ return {
529
+ success: true,
530
+ message: "Connection test failed",
531
+ data: {
532
+ connected: false,
533
+ error: error instanceof Error ? error.message : "Unknown error",
534
+ },
535
+ };
536
+ }
537
+ } catch (error) {
538
+ console.error("Failed to test metrics server connection:", error);
539
+ return {
540
+ success: false,
541
+ message: "Failed to test metrics server connection",
542
+ data: {
543
+ connected: false,
544
+ error: error instanceof Error ? error.message : "Unknown error",
545
+ },
546
+ };
547
+ }
548
+ }
549
+
550
+ /**
551
+ * List available metrics from a server
552
+ */
553
+ export async function listAvailableMetrics(
554
+ request: FastifyRequest,
555
+ reply: FastifyReply
556
+ ): Promise<ListMetricsResponse> {
557
+ const tenantId = getTenantId(request);
558
+ const { key } = request.params as { key: string };
559
+
560
+ try {
561
+ const storeLattice = getStoreLattice("default", "metrics");
562
+ const store: MetricsServerConfigStore = storeLattice.store;
563
+
564
+ const config = await store.getConfigByKey(tenantId, key);
565
+ if (!config) {
566
+ reply.code(404);
567
+ return {
568
+ success: false,
569
+ message: "Metrics server configuration not found",
570
+ };
571
+ }
572
+
573
+ // Ensure server is registered
574
+ if (!metricsServerManager.hasServer(key)) {
575
+ metricsServerManager.registerServer(key, config.config);
576
+ }
577
+
578
+ const client = metricsServerManager.getClient(key);
579
+ const metrics = await client.listMetrics();
580
+
581
+ return {
582
+ success: true,
583
+ message: "Metrics retrieved successfully",
584
+ data: {
585
+ metrics: metrics.map(m => ({
586
+ name: m.name,
587
+ type: m.type,
588
+ description: m.description,
589
+ })),
590
+ },
591
+ };
592
+ } catch (error) {
593
+ console.error("Failed to list metrics:", error);
594
+ return {
595
+ success: false,
596
+ message: "Failed to retrieve metrics",
597
+ };
598
+ }
599
+ }
600
+
601
+ /**
602
+ * Query metric data from a server
603
+ */
604
+ export async function queryMetricsData(
605
+ request: FastifyRequest,
606
+ reply: FastifyReply
607
+ ): Promise<QueryMetricDataResponse> {
608
+ const tenantId = getTenantId(request);
609
+ const { key } = request.params as { key: string };
610
+ const { metricName, startTime, endTime, step, labels } = request.body as {
611
+ metricName: string;
612
+ startTime?: number;
613
+ endTime?: number;
614
+ step?: number;
615
+ labels?: Record<string, string>;
616
+ };
617
+
618
+ try {
619
+ const storeLattice = getStoreLattice("default", "metrics");
620
+ const store: MetricsServerConfigStore = storeLattice.store;
621
+
622
+ const config = await store.getConfigByKey(tenantId, key);
623
+ if (!config) {
624
+ reply.code(404);
625
+ return {
626
+ success: false,
627
+ message: "Metrics server configuration not found",
628
+ };
629
+ }
630
+
631
+ if (!metricName) {
632
+ reply.code(400);
633
+ return {
634
+ success: false,
635
+ message: "metricName is required",
636
+ };
637
+ }
638
+
639
+ // Ensure server is registered
640
+ if (!metricsServerManager.hasServer(key)) {
641
+ metricsServerManager.registerServer(key, config.config);
642
+ }
643
+
644
+ const client = metricsServerManager.getClient(key);
645
+ const result = await client.queryMetricData(metricName, {
646
+ startTime,
647
+ endTime,
648
+ step,
649
+ labels,
650
+ });
651
+
652
+ return {
653
+ success: true,
654
+ message: "Metric data retrieved successfully",
655
+ data: {
656
+ metricName: result.metricName,
657
+ dataPoints: result.dataPoints,
658
+ },
659
+ };
660
+ } catch (error) {
661
+ console.error("Failed to query metric data:", error);
662
+ return {
663
+ success: false,
664
+ message: "Failed to query metric data",
665
+ };
666
+ }
667
+ }
668
+
669
+ /**
670
+ * Get data sources for a semantic metrics server
671
+ * GET /api/metrics-configs/:key/datasources
672
+ */
673
+ export async function getDataSources(
674
+ request: FastifyRequest,
675
+ reply: FastifyReply
676
+ ): Promise<DataSourcesResponse> {
677
+ const tenantId = getTenantId(request);
678
+ const { key } = request.params as { key: string };
679
+
680
+ try {
681
+ const storeLattice = getStoreLattice("default", "metrics");
682
+ const store: MetricsServerConfigStore = storeLattice.store;
683
+
684
+ const config = await store.getConfigByKey(tenantId, key);
685
+ if (!config) {
686
+ reply.code(404);
687
+ return {
688
+ success: false,
689
+ message: "Metrics server configuration not found",
690
+ };
691
+ }
692
+
693
+ if (config.config.type !== "semantic") {
694
+ reply.code(400);
695
+ return {
696
+ success: false,
697
+ message: "This endpoint is only available for semantic metrics servers",
698
+ };
699
+ }
700
+
701
+ const semanticConfig = config.config as SemanticMetricsServerConfig;
702
+
703
+ const client = new SemanticMetricsClient(semanticConfig);
704
+ const datasources = await client.getDataSources();
705
+
706
+ return {
707
+ success: true,
708
+ message: "Data sources retrieved successfully",
709
+ data: {
710
+ datasources,
711
+ },
712
+ };
713
+ } catch (error) {
714
+ console.error("Failed to get data sources:", error);
715
+ return {
716
+ success: false,
717
+ message: "Failed to retrieve data sources",
718
+ };
719
+ }
720
+ }
721
+
722
+ /**
723
+ * Get metrics for a specific data source
724
+ * GET /api/metrics-configs/:key/datasources/:datasourceId/metrics
725
+ */
726
+ export async function getDatasourceMetrics(
727
+ request: FastifyRequest,
728
+ reply: FastifyReply
729
+ ): Promise<DatasourceMetricsResponse> {
730
+ const tenantId = getTenantId(request);
731
+ const { key, datasourceId } = request.params as { key: string; datasourceId: string };
732
+
733
+ try {
734
+ const storeLattice = getStoreLattice("default", "metrics");
735
+ const store: MetricsServerConfigStore = storeLattice.store;
736
+
737
+ const config = await store.getConfigByKey(tenantId, key);
738
+ if (!config) {
739
+ reply.code(404);
740
+ return {
741
+ success: false,
742
+ message: "Metrics server configuration not found",
743
+ };
744
+ }
745
+
746
+ if (config.config.type !== "semantic") {
747
+ reply.code(400);
748
+ return {
749
+ success: false,
750
+ message: "This endpoint is only available for semantic metrics servers",
751
+ };
752
+ }
753
+
754
+ const semanticConfig = config.config as SemanticMetricsServerConfig;
755
+
756
+ const client = new SemanticMetricsClient(semanticConfig);
757
+ const metrics = await client.getDatasourceMetrics(datasourceId);
758
+
759
+ return {
760
+ success: true,
761
+ message: "Datasource metrics retrieved successfully",
762
+ data: metrics,
763
+ };
764
+ } catch (error) {
765
+ console.error("Failed to get datasource metrics:", error);
766
+ return {
767
+ success: false,
768
+ message: "Failed to retrieve datasource metrics",
769
+ };
770
+ }
771
+ }
772
+
773
+ /**
774
+ * Query semantic metrics data
775
+ * POST /api/metrics-configs/:key/semantic-query
776
+ */
777
+ export async function querySemanticMetrics(
778
+ request: FastifyRequest,
779
+ reply: FastifyReply
780
+ ): Promise<SemanticQueryResponse> {
781
+ const tenantId = getTenantId(request);
782
+ const { key } = request.params as { key: string };
783
+ const body = request.body as SemanticMetricsQueryRequest;
784
+
785
+ try {
786
+ const storeLattice = getStoreLattice("default", "metrics");
787
+ const store: MetricsServerConfigStore = storeLattice.store;
788
+
789
+ const config = await store.getConfigByKey(tenantId, key);
790
+ if (!config) {
791
+ reply.code(404);
792
+ return {
793
+ success: false,
794
+ message: "Metrics server configuration not found",
795
+ };
796
+ }
797
+
798
+ if (config.config.type !== "semantic") {
799
+ reply.code(400);
800
+ return {
801
+ success: false,
802
+ message: "This endpoint is only available for semantic metrics servers",
803
+ };
804
+ }
805
+
806
+ if (!body.datasourceId || !body.metrics || body.metrics.length === 0) {
807
+ reply.code(400);
808
+ return {
809
+ success: false,
810
+ message: "datasourceId and metrics array are required",
811
+ };
812
+ }
813
+
814
+ const semanticConfig = config.config as SemanticMetricsServerConfig;
815
+
816
+ const client = new SemanticMetricsClient(semanticConfig);
817
+ const result = await client.semanticQuery(body);
818
+
819
+ return {
820
+ success: true,
821
+ message: "Semantic query executed successfully",
822
+ data: {
823
+ datasourceId: result.datasourceId,
824
+ metrics: result.metrics,
825
+ dataPoints: result.dataPoints,
826
+ metadata: result.metadata,
827
+ },
828
+ };
829
+ } catch (error) {
830
+ console.error("Failed to query semantic metrics:", error);
831
+ return {
832
+ success: false,
833
+ message: "Failed to query semantic metrics",
834
+ };
835
+ }
836
+ }
837
+
838
+ /**
839
+ * Test datasources endpoint without saving config
840
+ * Used for multi-step configuration flow
841
+ * POST /api/metrics-configs/test-datasources
842
+ */
843
+ export async function testSemanticDataSources(
844
+ request: FastifyRequest,
845
+ reply: FastifyReply
846
+ ): Promise<DataSourcesResponse> {
847
+ const body = request.body as {
848
+ serverUrl: string;
849
+ apiKey?: string;
850
+ username?: string;
851
+ password?: string;
852
+ headers?: Record<string, string>;
853
+ };
854
+
855
+ try {
856
+ if (!body.serverUrl) {
857
+ reply.code(400);
858
+ return {
859
+ success: false,
860
+ message: "serverUrl is required",
861
+ };
862
+ }
863
+
864
+ const testConfig: SemanticMetricsServerConfig = {
865
+ type: "semantic",
866
+ serverUrl: body.serverUrl,
867
+ apiKey: body.apiKey,
868
+ username: body.username,
869
+ password: body.password,
870
+ headers: body.headers,
871
+ };
872
+
873
+ const client = new SemanticMetricsClient(testConfig);
874
+
875
+ const datasources = await client.getDataSources();
876
+
877
+ return {
878
+ success: true,
879
+ message: "Data sources retrieved successfully",
880
+ data: {
881
+ datasources,
882
+ },
883
+ };
884
+ } catch (error) {
885
+ console.error("Failed to test datasources:", error);
886
+ return {
887
+ success: false,
888
+ message: `Failed to retrieve data sources: ${error instanceof Error ? error.message : String(error)}`,
889
+ };
890
+ }
891
+ }
892
+
893
+ /**
894
+ * Test datasource metrics endpoint without saving config
895
+ * Used for multi-step configuration flow
896
+ * POST /api/metrics-configs/test-datasources/:datasourceId/metrics
897
+ */
898
+ export async function testDatasourceMetrics(
899
+ request: FastifyRequest,
900
+ reply: FastifyReply
901
+ ): Promise<DatasourceMetricsResponse> {
902
+ const { datasourceId } = request.params as { datasourceId: string };
903
+ const body = request.body as {
904
+ serverUrl: string;
905
+ apiKey?: string;
906
+ username?: string;
907
+ password?: string;
908
+ headers?: Record<string, string>;
909
+ };
910
+
911
+ try {
912
+ if (!body.serverUrl) {
913
+ reply.code(400);
914
+ return {
915
+ success: false,
916
+ message: "serverUrl is required",
917
+ };
918
+ }
919
+
920
+ const testConfig: SemanticMetricsServerConfig = {
921
+ type: "semantic",
922
+ serverUrl: body.serverUrl,
923
+ apiKey: body.apiKey,
924
+ username: body.username,
925
+ password: body.password,
926
+ headers: body.headers,
927
+ };
928
+
929
+ const client = new SemanticMetricsClient(testConfig);
930
+ const metrics = await client.getDatasourceMetrics(datasourceId);
931
+
932
+ return {
933
+ success: true,
934
+ message: "Datasource metrics retrieved successfully",
935
+ data: metrics,
936
+ };
937
+ } catch (error) {
938
+ console.error("Failed to test datasource metrics:", error);
939
+ return {
940
+ success: false,
941
+ message: `Failed to retrieve datasource metrics: ${error instanceof Error ? error.message : String(error)}`,
942
+ };
943
+ }
944
+ }
945
+
946
+ /**
947
+ * Register metrics server config routes
948
+ */
949
+ export function registerMetricsServerConfigRoutes(app: any): void {
950
+ // Get all configs
951
+ app.get("/api/metrics-configs", getMetricsServerConfigList);
952
+
953
+ // Get config by key
954
+ app.get("/api/metrics-configs/:key", getMetricsServerConfig);
955
+
956
+ // Create config
957
+ app.post("/api/metrics-configs", createMetricsServerConfig);
958
+
959
+ // Update config
960
+ app.put("/api/metrics-configs/:key", updateMetricsServerConfig);
961
+
962
+ // Delete config by key or id
963
+ app.delete("/api/metrics-configs/:keyOrId", deleteMetricsServerConfig);
964
+
965
+ // Test connection
966
+ app.post("/api/metrics-configs/:key/test", testMetricsServerConnection);
967
+
968
+ // List available metrics
969
+ app.get("/api/metrics-configs/:key/metrics", listAvailableMetrics);
970
+
971
+ // Query metric data
972
+ app.post("/api/metrics-configs/:key/query", queryMetricsData);
973
+
974
+ // Semantic-specific routes
975
+ // Get data sources for a semantic server
976
+ app.get("/api/metrics-configs/:key/datasources", getDataSources);
977
+
978
+ // Get metrics meta for a specific datasource
979
+ app.get("/api/metrics-configs/:key/datasources/:datasourceId/meta", getDatasourceMetrics);
980
+
981
+ // Query semantic metrics data
982
+ app.post("/api/metrics-configs/:key/semantic-query", querySemanticMetrics);
983
+
984
+ // Test datasources without saving config (for multi-step config flow)
985
+ app.post("/api/metrics-configs/test-datasources", testSemanticDataSources);
986
+
987
+ // Test datasource metrics meta without saving config (for multi-step config flow)
988
+ app.post("/api/metrics-configs/test-datasources/:datasourceId/meta", testDatasourceMetrics);
989
+ }