@oneuptime/common 10.5.9 → 10.5.18
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/Models/AnalyticsModels/ExceptionInstance.ts +1 -1
- package/Models/AnalyticsModels/Log.ts +1 -1
- package/Models/AnalyticsModels/Metric.ts +1 -1
- package/Models/AnalyticsModels/Profile.ts +1 -1
- package/Models/AnalyticsModels/ProfileSample.ts +1 -1
- package/Models/AnalyticsModels/Span.ts +1 -1
- package/Models/DatabaseModels/TelemetryException.ts +46 -34
- package/Models/DatabaseModels/TelemetryUsageBilling.ts +35 -2
- package/Server/API/AIAgentDataAPI.ts +25 -7
- package/Server/API/TelemetryAPI.ts +6 -0
- package/Server/API/TelemetryExceptionAPI.ts +6 -2
- package/Server/EnvironmentConfig.ts +27 -0
- package/Server/Infrastructure/ClickhouseDatabase.ts +21 -1
- package/Server/Infrastructure/Postgres/DataSourceOptions.ts +19 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.ts +28 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.ts +24 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.ts +47 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.ts +34 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Infrastructure/PostgresDatabase.ts +27 -1
- package/Server/Infrastructure/QueueWorker.ts +54 -4
- package/Server/Infrastructure/Redis.ts +11 -0
- package/Server/Services/AnalyticsDatabaseService.ts +87 -0
- package/Server/Services/DatabaseService.ts +73 -0
- package/Server/Services/TelemetryAttributeService.ts +38 -2
- package/Server/Services/TelemetryExceptionService.ts +24 -49
- package/Server/Services/TelemetryUsageBillingService.ts +289 -166
- package/Server/Types/AnalyticsDatabase/ModelPermission.ts +102 -72
- package/Server/Types/Database/Permissions/OwnedScopePermission.ts +81 -60
- package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +67 -0
- package/Server/Utils/Express.ts +32 -0
- package/Server/Utils/GracefulShutdown.ts +194 -0
- package/Server/Utils/Logger.ts +12 -1
- package/Server/Utils/Monitor/MonitorLogUtil.ts +22 -17
- package/Server/Utils/Profiling.ts +14 -6
- package/Server/Utils/StartServer.ts +13 -5
- package/Server/Utils/Telemetry/ContextSpanProcessor.ts +48 -0
- package/Server/Utils/Telemetry/LogExceptionExtractor.ts +289 -0
- package/Server/Utils/Telemetry/SpanUtil.ts +16 -35
- package/Server/Utils/Telemetry/StackTraceParser.ts +423 -0
- package/Server/Utils/Telemetry/TelemetryContext.ts +190 -0
- package/Server/Utils/Telemetry.ts +33 -7
- package/Tests/Server/Services/TelemetryAttributeService.test.ts +83 -0
- package/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.ts +0 -0
- package/Types/Database/AccessControl/OwnedThrough.ts +31 -3
- package/Types/Telemetry/ServiceType.ts +10 -0
- package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +7 -1
- package/UI/Components/Dictionary/Dictionary.tsx +19 -0
- package/UI/Components/Filters/FiltersForm.tsx +1 -0
- package/UI/Components/Filters/JSONFilter.tsx +2 -0
- package/UI/Components/Filters/Types/Filter.ts +1 -0
- package/UI/Components/LogsViewer/LogsViewer.tsx +16 -0
- package/UI/Utils/Project.ts +6 -0
- package/UI/Utils/Telemetry/Telemetry.ts +65 -0
- package/UI/Utils/TelemetryService.ts +150 -0
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +1 -1
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Log.js +1 -1
- package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Metric.js +1 -1
- package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Profile.js +1 -1
- package/build/dist/Models/AnalyticsModels/Profile.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/ProfileSample.js +1 -1
- package/build/dist/Models/AnalyticsModels/ProfileSample.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Span.js +1 -1
- package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryException.js +47 -33
- package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryUsageBilling.js +36 -2
- package/build/dist/Models/DatabaseModels/TelemetryUsageBilling.js.map +1 -1
- package/build/dist/Server/API/AIAgentDataAPI.js +24 -8
- package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +4 -0
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryExceptionAPI.js +6 -2
- package/build/dist/Server/API/TelemetryExceptionAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +19 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/ClickhouseDatabase.js +16 -2
- package/build/dist/Server/Infrastructure/ClickhouseDatabase.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +10 -9
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.js +23 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.js +19 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.js +25 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/PostgresDatabase.js +20 -1
- package/build/dist/Server/Infrastructure/PostgresDatabase.js.map +1 -1
- package/build/dist/Server/Infrastructure/QueueWorker.js +40 -3
- package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
- package/build/dist/Server/Infrastructure/Redis.js +5 -0
- package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +59 -0
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +62 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryAttributeService.js +23 -1
- package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryExceptionService.js +16 -41
- package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryUsageBillingService.js +211 -147
- package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +84 -63
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/OwnedScopePermission.js +67 -49
- package/build/dist/Server/Types/Database/Permissions/OwnedScopePermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +51 -0
- package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Express.js +23 -0
- package/build/dist/Server/Utils/Express.js.map +1 -1
- package/build/dist/Server/Utils/GracefulShutdown.js +145 -0
- package/build/dist/Server/Utils/GracefulShutdown.js.map +1 -0
- package/build/dist/Server/Utils/Logger.js +8 -1
- package/build/dist/Server/Utils/Logger.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -10
- package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
- package/build/dist/Server/Utils/Profiling.js +8 -3
- package/build/dist/Server/Utils/Profiling.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +12 -4
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ContextSpanProcessor.js +37 -0
- package/build/dist/Server/Utils/Telemetry/ContextSpanProcessor.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js +214 -0
- package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/SpanUtil.js +15 -24
- package/build/dist/Server/Utils/Telemetry/SpanUtil.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/StackTraceParser.js +365 -0
- package/build/dist/Server/Utils/Telemetry/StackTraceParser.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryContext.js +124 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryContext.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry.js +22 -5
- package/build/dist/Server/Utils/Telemetry.js.map +1 -1
- package/build/dist/Tests/Server/Services/TelemetryAttributeService.test.js +50 -0
- package/build/dist/Tests/Server/Services/TelemetryAttributeService.test.js.map +1 -0
- package/build/dist/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.js +0 -0
- package/build/dist/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.js.map +1 -0
- package/build/dist/Types/Database/AccessControl/OwnedThrough.js +7 -2
- package/build/dist/Types/Database/AccessControl/OwnedThrough.js.map +1 -1
- package/build/dist/Types/Telemetry/ServiceType.js +10 -0
- package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +7 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/Dictionary.js +10 -0
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Filters/FiltersForm.js +1 -1
- package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
- package/build/dist/UI/Components/Filters/JSONFilter.js +1 -1
- package/build/dist/UI/Components/Filters/JSONFilter.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +15 -0
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Utils/Project.js +5 -0
- package/build/dist/UI/Utils/Project.js.map +1 -1
- package/build/dist/UI/Utils/Telemetry/Telemetry.js +44 -0
- package/build/dist/UI/Utils/Telemetry/Telemetry.js.map +1 -1
- package/build/dist/UI/Utils/TelemetryService.js +113 -0
- package/build/dist/UI/Utils/TelemetryService.js.map +1 -0
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import Sleep from "../../Types/Sleep";
|
|
|
4
4
|
import { DataSource, DataSourceOptions } from "typeorm";
|
|
5
5
|
import { createDatabase, dropDatabase } from "typeorm-extension";
|
|
6
6
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
7
|
+
import GracefulShutdown, { ShutdownPriority } from "../Utils/GracefulShutdown";
|
|
7
8
|
|
|
8
9
|
export type DatabaseSourceOptions = DataSourceOptions;
|
|
9
10
|
export type DatabaseSource = DataSource;
|
|
@@ -30,6 +31,15 @@ export default class Database {
|
|
|
30
31
|
|
|
31
32
|
@CaptureSpan()
|
|
32
33
|
public static async connect(): Promise<DataSource> {
|
|
34
|
+
/*
|
|
35
|
+
* Idempotent: a second connect() must not overwrite (and thereby orphan)
|
|
36
|
+
* the existing pool. Return the live DataSource instead of building a new
|
|
37
|
+
* one.
|
|
38
|
+
*/
|
|
39
|
+
if (this.dataSource) {
|
|
40
|
+
return this.dataSource;
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
let retry: number = 0;
|
|
34
44
|
|
|
35
45
|
const dataSourceOptions: DataSourceOptions = this.getDatasourceOptions();
|
|
@@ -64,7 +74,23 @@ export default class Database {
|
|
|
64
74
|
}
|
|
65
75
|
};
|
|
66
76
|
|
|
67
|
-
|
|
77
|
+
const dataSource: DataSource = await connectToDatabase();
|
|
78
|
+
|
|
79
|
+
/*
|
|
80
|
+
* Drain the pool on shutdown. Registered here (after a successful
|
|
81
|
+
* connect) so we never register cleanup for a pool that was never
|
|
82
|
+
* created, and — thanks to GracefulShutdown deduping by name — exactly
|
|
83
|
+
* once even if connect() is somehow reached twice.
|
|
84
|
+
*/
|
|
85
|
+
GracefulShutdown.registerHandler(
|
|
86
|
+
"PostgresDatabase",
|
|
87
|
+
ShutdownPriority.DataStores,
|
|
88
|
+
() => {
|
|
89
|
+
return this.disconnect();
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return dataSource;
|
|
68
94
|
} catch (err) {
|
|
69
95
|
logger.error("Postgres Database Connection Failed");
|
|
70
96
|
logger.error(err);
|
|
@@ -8,7 +8,14 @@ import {
|
|
|
8
8
|
import { Worker } from "bullmq";
|
|
9
9
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
10
10
|
import AppMetrics from "../Utils/Telemetry/AppMetrics";
|
|
11
|
+
import TelemetryContext from "../Utils/Telemetry/TelemetryContext";
|
|
12
|
+
import Telemetry, {
|
|
13
|
+
Span,
|
|
14
|
+
SpanException,
|
|
15
|
+
SpanStatusCode,
|
|
16
|
+
} from "../Utils/Telemetry";
|
|
11
17
|
import Redis from "./Redis";
|
|
18
|
+
import GracefulShutdown, { ShutdownPriority } from "../Utils/GracefulShutdown";
|
|
12
19
|
|
|
13
20
|
export default class QueueWorker {
|
|
14
21
|
@CaptureSpan()
|
|
@@ -45,7 +52,40 @@ export default class QueueWorker {
|
|
|
45
52
|
let outcome: "success" | "failure" | "timeout" = "success";
|
|
46
53
|
|
|
47
54
|
try {
|
|
48
|
-
|
|
55
|
+
/*
|
|
56
|
+
* Seed a telemetry-context scope for this job so every span and log it
|
|
57
|
+
* produces inherits the queue/job name plus any tenant identifiers
|
|
58
|
+
* carried in the job payload (projectId, monitorId, incidentId, ...).
|
|
59
|
+
*/
|
|
60
|
+
await TelemetryContext.runWithContext(
|
|
61
|
+
{
|
|
62
|
+
queueName: queueName,
|
|
63
|
+
jobName: job.name || "unknown",
|
|
64
|
+
...TelemetryContext.pickKnownAttributes(job.data),
|
|
65
|
+
},
|
|
66
|
+
() => {
|
|
67
|
+
/*
|
|
68
|
+
* Wrap the job in an explicit root span so every background job has
|
|
69
|
+
* a consistent, named trace root that carries the seeded context —
|
|
70
|
+
* the @CaptureSpan service calls it makes become children of this.
|
|
71
|
+
*/
|
|
72
|
+
return Telemetry.startActiveSpan<Promise<void>>({
|
|
73
|
+
name: `worker.job ${queueName}/${job.name || "unknown"}`,
|
|
74
|
+
fn: async (span: Span): Promise<void> => {
|
|
75
|
+
try {
|
|
76
|
+
await onJobInQueue(job);
|
|
77
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
78
|
+
} catch (err) {
|
|
79
|
+
span.recordException(err as SpanException);
|
|
80
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
81
|
+
throw err;
|
|
82
|
+
} finally {
|
|
83
|
+
span.end();
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
);
|
|
49
89
|
} catch (err) {
|
|
50
90
|
outcome =
|
|
51
91
|
err instanceof TimeoutException ||
|
|
@@ -77,9 +117,19 @@ export default class QueueWorker {
|
|
|
77
117
|
: {}),
|
|
78
118
|
});
|
|
79
119
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
120
|
+
/*
|
|
121
|
+
* Stop pulling new jobs and let in-flight ones finish on shutdown. Runs in
|
|
122
|
+
* the Workers tier — before datastores are drained — so jobs mid-flight can
|
|
123
|
+
* still reach Postgres / Redis. Replaces a SIGINT-only handler that never
|
|
124
|
+
* fired in containers (Kubernetes / docker stop send SIGTERM).
|
|
125
|
+
*/
|
|
126
|
+
GracefulShutdown.registerHandler(
|
|
127
|
+
`QueueWorker:${queueName}`,
|
|
128
|
+
ShutdownPriority.Workers,
|
|
129
|
+
() => {
|
|
130
|
+
return worker.close();
|
|
131
|
+
},
|
|
132
|
+
);
|
|
83
133
|
|
|
84
134
|
return worker;
|
|
85
135
|
}
|
|
@@ -15,6 +15,7 @@ import logger from "../Utils/Logger";
|
|
|
15
15
|
import Sleep from "../../Types/Sleep";
|
|
16
16
|
import { Redis as RedisClient, RedisOptions } from "ioredis";
|
|
17
17
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
18
|
+
import GracefulShutdown, { ShutdownPriority } from "../Utils/GracefulShutdown";
|
|
18
19
|
|
|
19
20
|
export type ClientType = RedisClient;
|
|
20
21
|
export type RedisOptionsType = RedisOptions;
|
|
@@ -122,6 +123,16 @@ export default abstract class Redis {
|
|
|
122
123
|
logger.debug(
|
|
123
124
|
`Redis connected on ${RedisHostname}:${RedisPort.toNumber()}`,
|
|
124
125
|
);
|
|
126
|
+
|
|
127
|
+
// Close the Redis connection on shutdown.
|
|
128
|
+
GracefulShutdown.registerHandler(
|
|
129
|
+
"Redis",
|
|
130
|
+
ShutdownPriority.DataStores,
|
|
131
|
+
() => {
|
|
132
|
+
return this.disconnect();
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
|
|
125
136
|
return this.client;
|
|
126
137
|
} catch (err) {
|
|
127
138
|
logger.error("Redis Connection Failed");
|
|
@@ -366,6 +366,93 @@ export default class AnalyticsDatabaseService<
|
|
|
366
366
|
return await this._findBy(findBy);
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Group telemetry rows by (serviceId, serviceType) for a project over a
|
|
371
|
+
* time window, returning the row count and an estimate of the ingested
|
|
372
|
+
* byte size (ClickHouse `byteSize(*)`, the uncompressed in-memory size of
|
|
373
|
+
* each row's columns). This is the enumeration source for usage billing:
|
|
374
|
+
* a single aggregation scan surfaces EVERY resource that emitted
|
|
375
|
+
* telemetry — real Services, Hosts, Docker hosts, Kubernetes clusters,
|
|
376
|
+
* Monitors and unattributed (serviceId = projectId) — without needing a
|
|
377
|
+
* Postgres row per resource. The caller decides which serviceTypes to
|
|
378
|
+
* bill and how to attribute retention.
|
|
379
|
+
*/
|
|
380
|
+
@CaptureSpan()
|
|
381
|
+
public async groupTelemetryUsageByService(data: {
|
|
382
|
+
projectId: ObjectID;
|
|
383
|
+
timestampColumnName: keyof TBaseModel | string;
|
|
384
|
+
startDate: Date;
|
|
385
|
+
endDate: Date;
|
|
386
|
+
}): Promise<
|
|
387
|
+
Array<{
|
|
388
|
+
serviceId: string;
|
|
389
|
+
serviceType: string | null;
|
|
390
|
+
rowCount: number;
|
|
391
|
+
estimatedBytes: number;
|
|
392
|
+
}>
|
|
393
|
+
> {
|
|
394
|
+
const timestampColumnName: string = data.timestampColumnName.toString();
|
|
395
|
+
|
|
396
|
+
if (!this.model.getTableColumn(timestampColumnName)) {
|
|
397
|
+
throw new BadDataException(
|
|
398
|
+
`Invalid timestampColumnName: ${timestampColumnName}`,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!this.database) {
|
|
403
|
+
this.useDefaultDatabase();
|
|
404
|
+
}
|
|
405
|
+
const databaseName: string =
|
|
406
|
+
this.database!.getDatasourceOptions().database!;
|
|
407
|
+
|
|
408
|
+
const statement: Statement = SQL`SELECT serviceId AS serviceId, serviceType AS serviceType, count() AS rowCount, sum(byteSize(*)) AS estimatedBytes FROM ${databaseName}.${this.model.tableName} WHERE projectId = ${{
|
|
409
|
+
type: TableColumnType.ObjectID,
|
|
410
|
+
value: data.projectId,
|
|
411
|
+
}} AND ${timestampColumnName} >= ${{
|
|
412
|
+
type: TableColumnType.DateTime64,
|
|
413
|
+
value: data.startDate,
|
|
414
|
+
}} AND ${timestampColumnName} <= ${{
|
|
415
|
+
type: TableColumnType.DateTime64,
|
|
416
|
+
value: data.endDate,
|
|
417
|
+
}} GROUP BY serviceId, serviceType`;
|
|
418
|
+
|
|
419
|
+
statement.append(
|
|
420
|
+
" SETTINGS max_execution_time = 60, timeout_overflow_mode = 'break'",
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
const dbResult: ResultSet<"JSON"> = await this.executeQuery(statement);
|
|
424
|
+
const responseJSON: ResponseJSON<JSONObject> =
|
|
425
|
+
await dbResult.json<JSONObject>();
|
|
426
|
+
const items: Array<JSONObject> = responseJSON.data ? responseJSON.data : [];
|
|
427
|
+
|
|
428
|
+
const results: Array<{
|
|
429
|
+
serviceId: string;
|
|
430
|
+
serviceType: string | null;
|
|
431
|
+
rowCount: number;
|
|
432
|
+
estimatedBytes: number;
|
|
433
|
+
}> = [];
|
|
434
|
+
|
|
435
|
+
for (const item of items) {
|
|
436
|
+
const serviceId: string = (item["serviceId"] as string) || "";
|
|
437
|
+
if (!serviceId) {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const serviceTypeRaw: unknown = item["serviceType"];
|
|
441
|
+
const serviceType: string | null =
|
|
442
|
+
typeof serviceTypeRaw === "string" && serviceTypeRaw.trim()
|
|
443
|
+
? serviceTypeRaw
|
|
444
|
+
: null;
|
|
445
|
+
results.push({
|
|
446
|
+
serviceId,
|
|
447
|
+
serviceType,
|
|
448
|
+
rowCount: Number(item["rowCount"]) || 0,
|
|
449
|
+
estimatedBytes: Number(item["estimatedBytes"]) || 0,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return results;
|
|
454
|
+
}
|
|
455
|
+
|
|
369
456
|
@CaptureSpan()
|
|
370
457
|
public async aggregateBy(
|
|
371
458
|
aggregateBy: AggregateBy<TBaseModel>,
|
|
@@ -54,6 +54,7 @@ import HashedString from "../../Types/HashedString";
|
|
|
54
54
|
import { JSONObject, JSONValue } from "../../Types/JSON";
|
|
55
55
|
import JSONFunctions from "../../Types/JSONFunctions";
|
|
56
56
|
import ObjectID from "../../Types/ObjectID";
|
|
57
|
+
import TelemetryContext from "../Utils/Telemetry/TelemetryContext";
|
|
57
58
|
import PositiveNumber from "../../Types/PositiveNumber";
|
|
58
59
|
import Text from "../../Types/Text";
|
|
59
60
|
import Typeof from "../../Types/Typeof";
|
|
@@ -681,6 +682,69 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
|
681
682
|
}
|
|
682
683
|
}
|
|
683
684
|
|
|
685
|
+
/**
|
|
686
|
+
* Derive the telemetry attribute key for this model's primary id, e.g.
|
|
687
|
+
* `Incident` -> `incidentId`, `Monitor` -> `monitorId`. Matches the keys in
|
|
688
|
+
* TelemetryContextAttributes so dashboards/queries stay consistent.
|
|
689
|
+
*/
|
|
690
|
+
private getTelemetryEntityIdKey(): string {
|
|
691
|
+
const name: string = this.modelName || "entity";
|
|
692
|
+
return name.charAt(0).toLowerCase() + name.slice(1) + "Id";
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Seed the ambient telemetry context with the project (tenant) of an
|
|
697
|
+
* operation so worker/cron/service spans and logs — which run outside the
|
|
698
|
+
* HTTP request scope — still carry projectId. Best-effort and safe to call
|
|
699
|
+
* anywhere.
|
|
700
|
+
*/
|
|
701
|
+
protected setTelemetryContextFromProps(
|
|
702
|
+
props: DatabaseCommonInteractionProps | undefined,
|
|
703
|
+
): void {
|
|
704
|
+
try {
|
|
705
|
+
if (props?.tenantId) {
|
|
706
|
+
TelemetryContext.setAttributes({
|
|
707
|
+
projectId: props.tenantId.toString(),
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
} catch {
|
|
711
|
+
// Telemetry must never break a database operation.
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Seed the ambient telemetry context from a concrete model instance: the
|
|
717
|
+
* project (tenant) and the entity's own id (e.g. incidentId, monitorId).
|
|
718
|
+
* Used on create, where a new entity id is minted. Best-effort and safe.
|
|
719
|
+
*/
|
|
720
|
+
protected setTelemetryContextFromItem(item: TBaseModel | undefined): void {
|
|
721
|
+
try {
|
|
722
|
+
if (!item) {
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const attributes: { [key: string]: string } = {};
|
|
727
|
+
|
|
728
|
+
const tenantColumn: string | null = this.model.getTenantColumn();
|
|
729
|
+
if (tenantColumn) {
|
|
730
|
+
const tenantValue: unknown = item.getColumnValue(tenantColumn);
|
|
731
|
+
if (tenantValue) {
|
|
732
|
+
attributes["projectId"] = String(tenantValue);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (item.id) {
|
|
737
|
+
attributes[this.getTelemetryEntityIdKey()] = item.id.toString();
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (Object.keys(attributes).length > 0) {
|
|
741
|
+
TelemetryContext.setAttributes(attributes);
|
|
742
|
+
}
|
|
743
|
+
} catch {
|
|
744
|
+
// Telemetry must never break a database operation.
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
684
748
|
@CaptureSpan()
|
|
685
749
|
public async create(createBy: CreateBy<TBaseModel>): Promise<TBaseModel> {
|
|
686
750
|
const onCreate: OnCreate<TBaseModel> = createBy.props.ignoreHooks
|
|
@@ -742,6 +806,9 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
|
742
806
|
try {
|
|
743
807
|
createBy.data = await this.getRepository().save(createBy.data);
|
|
744
808
|
|
|
809
|
+
// Seed telemetry context with projectId + <model>Id for this create.
|
|
810
|
+
this.setTelemetryContextFromItem(createBy.data);
|
|
811
|
+
|
|
745
812
|
if (!createBy.props.ignoreHooks) {
|
|
746
813
|
createBy.data = await this.onCreateSuccess(
|
|
747
814
|
{
|
|
@@ -1216,6 +1283,8 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
|
1216
1283
|
|
|
1217
1284
|
private async _deleteBy(deleteBy: DeleteBy<TBaseModel>): Promise<number> {
|
|
1218
1285
|
try {
|
|
1286
|
+
this.setTelemetryContextFromProps(deleteBy.props);
|
|
1287
|
+
|
|
1219
1288
|
if (this.doNotAllowDelete && !deleteBy.props.isRoot) {
|
|
1220
1289
|
throw new BadDataException("Delete not allowed");
|
|
1221
1290
|
}
|
|
@@ -1444,6 +1513,8 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
|
1444
1513
|
withDeleted?: boolean | undefined,
|
|
1445
1514
|
): Promise<Array<TBaseModel>> {
|
|
1446
1515
|
try {
|
|
1516
|
+
this.setTelemetryContextFromProps(findBy.props);
|
|
1517
|
+
|
|
1447
1518
|
let automaticallyAddedCreatedAtInSelect: boolean = false;
|
|
1448
1519
|
|
|
1449
1520
|
if (!findBy.sort || Object.keys(findBy.sort).length === 0) {
|
|
@@ -1638,6 +1709,8 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
|
1638
1709
|
|
|
1639
1710
|
private async _updateBy(updateBy: UpdateBy<TBaseModel>): Promise<number> {
|
|
1640
1711
|
try {
|
|
1712
|
+
this.setTelemetryContextFromProps(updateBy.props);
|
|
1713
|
+
|
|
1641
1714
|
const onUpdate: OnUpdate<TBaseModel> = updateBy.props.ignoreHooks
|
|
1642
1715
|
? { updateBy, carryForward: [] }
|
|
1643
1716
|
: await this.onBeforeUpdate(updateBy);
|
|
@@ -364,6 +364,7 @@ export class TelemetryAttributeService {
|
|
|
364
364
|
telemetryType: TelemetryType;
|
|
365
365
|
metricName?: string | undefined;
|
|
366
366
|
attributeKey: string;
|
|
367
|
+
searchText?: string | undefined;
|
|
367
368
|
}): Promise<string[]> {
|
|
368
369
|
const source: TelemetrySource | null = this.getTelemetrySource(
|
|
369
370
|
data.telemetryType,
|
|
@@ -378,15 +379,17 @@ export class TelemetryAttributeService {
|
|
|
378
379
|
source,
|
|
379
380
|
metricName: data.metricName,
|
|
380
381
|
attributeKey: data.attributeKey,
|
|
382
|
+
searchText: data.searchText,
|
|
381
383
|
});
|
|
382
384
|
}
|
|
383
385
|
|
|
384
|
-
private static
|
|
386
|
+
private static buildAttributeValuesStatement(data: {
|
|
385
387
|
projectId: ObjectID;
|
|
386
388
|
source: TelemetrySource;
|
|
387
389
|
metricName?: string | undefined;
|
|
388
390
|
attributeKey: string;
|
|
389
|
-
|
|
391
|
+
searchText?: string | undefined;
|
|
392
|
+
}): Statement {
|
|
390
393
|
const lookbackStartDate: Date =
|
|
391
394
|
TelemetryAttributeService.getLookbackStartDate();
|
|
392
395
|
|
|
@@ -419,6 +422,26 @@ export class TelemetryAttributeService {
|
|
|
419
422
|
);
|
|
420
423
|
}
|
|
421
424
|
|
|
425
|
+
/*
|
|
426
|
+
* Case-insensitive substring filter so the value autocomplete keeps
|
|
427
|
+
* narrowing server-side as the user types. Without it only the first
|
|
428
|
+
* ATTRIBUTE_VALUES_LIMIT values (alphabetically) are ever reachable,
|
|
429
|
+
* which hides matches on high-cardinality keys (host.name, url, ...).
|
|
430
|
+
* Mirrors the ILIKE idiom used for bodySearchText / nameSearchText.
|
|
431
|
+
*/
|
|
432
|
+
if (data.searchText && data.searchText.trim().length > 0) {
|
|
433
|
+
statement.append(
|
|
434
|
+
SQL`
|
|
435
|
+
AND ${data.source.attributesColumn}[${{
|
|
436
|
+
type: TableColumnType.Text,
|
|
437
|
+
value: data.attributeKey,
|
|
438
|
+
}}] ILIKE ${{
|
|
439
|
+
type: TableColumnType.Text,
|
|
440
|
+
value: `%${data.searchText.trim()}%`,
|
|
441
|
+
}}`,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
422
445
|
statement.append(
|
|
423
446
|
SQL`
|
|
424
447
|
ORDER BY attributeValue ASC
|
|
@@ -428,6 +451,19 @@ export class TelemetryAttributeService {
|
|
|
428
451
|
}}`,
|
|
429
452
|
);
|
|
430
453
|
|
|
454
|
+
return statement;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private static async fetchAttributeValuesFromDatabase(data: {
|
|
458
|
+
projectId: ObjectID;
|
|
459
|
+
source: TelemetrySource;
|
|
460
|
+
metricName?: string | undefined;
|
|
461
|
+
attributeKey: string;
|
|
462
|
+
searchText?: string | undefined;
|
|
463
|
+
}): Promise<Array<string>> {
|
|
464
|
+
const statement: Statement =
|
|
465
|
+
TelemetryAttributeService.buildAttributeValuesStatement(data);
|
|
466
|
+
|
|
431
467
|
const dbResult: Results = await data.source.service.executeQuery(statement);
|
|
432
468
|
const response: DbJSONResponse = await dbResult.json<{
|
|
433
469
|
data?: Array<JSONObject>;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import DatabaseService from "./DatabaseService";
|
|
2
2
|
import Model from "../../Models/DatabaseModels/TelemetryException";
|
|
3
|
-
import
|
|
3
|
+
import ServiceType from "../../Types/Telemetry/ServiceType";
|
|
4
4
|
import AIAgentTask from "../../Models/DatabaseModels/AIAgentTask";
|
|
5
5
|
import AIAgentTaskTelemetryException from "../../Models/DatabaseModels/AIAgentTaskTelemetryException";
|
|
6
6
|
import ObjectID from "../../Types/ObjectID";
|
|
7
|
-
import PositiveNumber from "../../Types/PositiveNumber";
|
|
8
7
|
import SortOrder from "../../Types/BaseDatabase/SortOrder";
|
|
9
8
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
10
9
|
import AIAgentTaskType from "../../Types/AI/AIAgentTaskType";
|
|
@@ -13,7 +12,6 @@ import { FixExceptionTaskMetadata } from "../../Types/AI/AIAgentTaskMetadata";
|
|
|
13
12
|
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
14
13
|
import AIAgentTaskService from "./AIAgentTaskService";
|
|
15
14
|
import AIAgentTaskTelemetryExceptionService from "./AIAgentTaskTelemetryExceptionService";
|
|
16
|
-
import ServiceService from "./ServiceService";
|
|
17
15
|
import QueryHelper from "../Types/Database/QueryHelper";
|
|
18
16
|
import ModelPermission from "../Types/Database/Permissions/Index";
|
|
19
17
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
@@ -24,7 +22,13 @@ export interface CreateAIAgentTaskForExceptionParams {
|
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
export interface DashboardServiceSummary {
|
|
27
|
-
|
|
25
|
+
/*
|
|
26
|
+
* Polymorphic: a real Service, a Host/DockerHost/KubernetesCluster id, or
|
|
27
|
+
* the projectId for unattributed telemetry. The client resolves the
|
|
28
|
+
* display name per serviceType.
|
|
29
|
+
*/
|
|
30
|
+
serviceId: string;
|
|
31
|
+
serviceType: ServiceType | null;
|
|
28
32
|
unresolvedCount: number;
|
|
29
33
|
totalOccurrences: number;
|
|
30
34
|
}
|
|
@@ -182,11 +186,9 @@ export class Service extends DatabaseService<Model> {
|
|
|
182
186
|
lastSeenAt: true,
|
|
183
187
|
firstSeenAt: true,
|
|
184
188
|
environment: true,
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
serviceColor: true,
|
|
189
|
-
},
|
|
189
|
+
// serviceId is polymorphic; the client resolves it per serviceType.
|
|
190
|
+
serviceId: true,
|
|
191
|
+
serviceType: true,
|
|
190
192
|
};
|
|
191
193
|
|
|
192
194
|
const [
|
|
@@ -284,6 +286,7 @@ export class Service extends DatabaseService<Model> {
|
|
|
284
286
|
|
|
285
287
|
interface AggregateRow {
|
|
286
288
|
serviceId: string | null;
|
|
289
|
+
serviceType: string | null;
|
|
287
290
|
unresolvedCount: string;
|
|
288
291
|
totalOccurrences: string | null;
|
|
289
292
|
}
|
|
@@ -292,6 +295,7 @@ export class Service extends DatabaseService<Model> {
|
|
|
292
295
|
"TelemetryException",
|
|
293
296
|
)
|
|
294
297
|
.select(`"TelemetryException"."serviceId"`, "serviceId")
|
|
298
|
+
.addSelect(`"TelemetryException"."serviceType"`, "serviceType")
|
|
295
299
|
.addSelect(`COUNT(*)`, "unresolvedCount")
|
|
296
300
|
.addSelect(
|
|
297
301
|
`COALESCE(SUM("TelemetryException"."occuranceCount"), 0)`,
|
|
@@ -305,6 +309,7 @@ export class Service extends DatabaseService<Model> {
|
|
|
305
309
|
.andWhere(`"TelemetryException"."deletedAt" IS NULL`)
|
|
306
310
|
.andWhere(`"TelemetryException"."serviceId" IS NOT NULL`)
|
|
307
311
|
.groupBy(`"TelemetryException"."serviceId"`)
|
|
312
|
+
.addGroupBy(`"TelemetryException"."serviceType"`)
|
|
308
313
|
.orderBy(`"unresolvedCount"`, "DESC")
|
|
309
314
|
.getRawMany()) as Array<AggregateRow>;
|
|
310
315
|
|
|
@@ -312,52 +317,22 @@ export class Service extends DatabaseService<Model> {
|
|
|
312
317
|
return [];
|
|
313
318
|
}
|
|
314
319
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
return [];
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const services: Array<TelemetryServiceModel> = await ServiceService.findBy({
|
|
327
|
-
query: {
|
|
328
|
-
projectId,
|
|
329
|
-
_id: QueryHelper.any(serviceIds),
|
|
330
|
-
},
|
|
331
|
-
select: {
|
|
332
|
-
_id: true,
|
|
333
|
-
name: true,
|
|
334
|
-
serviceColor: true,
|
|
335
|
-
},
|
|
336
|
-
limit: new PositiveNumber(serviceIds.length),
|
|
337
|
-
skip: new PositiveNumber(0),
|
|
338
|
-
props,
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
const serviceById: Map<string, TelemetryServiceModel> = new Map();
|
|
342
|
-
for (const service of services) {
|
|
343
|
-
if (service._id) {
|
|
344
|
-
serviceById.set(service._id, service);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
320
|
+
/*
|
|
321
|
+
* serviceId is polymorphic — do NOT resolve it to a Service here. The
|
|
322
|
+
* old code looked each serviceId up in the Service table and dropped
|
|
323
|
+
* any that didn't match, which silently excluded Host / DockerHost /
|
|
324
|
+
* KubernetesCluster and unattributed (Unknown) telemetry. Return the
|
|
325
|
+
* raw (serviceId, serviceType) + counts; the client resolves the
|
|
326
|
+
* display name per serviceType.
|
|
327
|
+
*/
|
|
348
328
|
const summaries: Array<DashboardServiceSummary> = [];
|
|
349
329
|
for (const row of rows) {
|
|
350
330
|
if (!row.serviceId) {
|
|
351
331
|
continue;
|
|
352
332
|
}
|
|
353
|
-
const service: TelemetryServiceModel | undefined = serviceById.get(
|
|
354
|
-
row.serviceId,
|
|
355
|
-
);
|
|
356
|
-
if (!service) {
|
|
357
|
-
continue;
|
|
358
|
-
}
|
|
359
333
|
summaries.push({
|
|
360
|
-
|
|
334
|
+
serviceId: row.serviceId,
|
|
335
|
+
serviceType: (row.serviceType as ServiceType | null) ?? null,
|
|
361
336
|
unresolvedCount: parseInt(row.unresolvedCount, 10) || 0,
|
|
362
337
|
totalOccurrences: parseInt(row.totalOccurrences || "0", 10) || 0,
|
|
363
338
|
});
|