@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
|
@@ -39,7 +39,11 @@ import {
|
|
|
39
39
|
} from "@opentelemetry/sdk-metrics";
|
|
40
40
|
import type { PushMetricExporter } from "@opentelemetry/sdk-metrics/build/src/export/MetricExporter";
|
|
41
41
|
import * as opentelemetry from "@opentelemetry/sdk-node";
|
|
42
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
BatchSpanProcessor,
|
|
44
|
+
SpanExporter,
|
|
45
|
+
SpanProcessor,
|
|
46
|
+
} from "@opentelemetry/sdk-trace-base";
|
|
43
47
|
import {
|
|
44
48
|
ATTR_SERVICE_NAME,
|
|
45
49
|
ATTR_SERVICE_VERSION,
|
|
@@ -48,6 +52,8 @@ import URL from "../../Types/API/URL";
|
|
|
48
52
|
import Dictionary from "../../Types/Dictionary";
|
|
49
53
|
import { AppVersion, Env, DisableTelemetry } from "../EnvironmentConfig";
|
|
50
54
|
import logger from "./Logger";
|
|
55
|
+
import GracefulShutdown, { ShutdownPriority } from "./GracefulShutdown";
|
|
56
|
+
import ContextSpanProcessor from "./Telemetry/ContextSpanProcessor";
|
|
51
57
|
import RuntimeMetrics from "./Telemetry/RuntimeMetrics";
|
|
52
58
|
|
|
53
59
|
type ResourceWithRawAttributes = LogsResource & {
|
|
@@ -268,10 +274,21 @@ export default class Telemetry {
|
|
|
268
274
|
autoDetectResources: true,
|
|
269
275
|
};
|
|
270
276
|
|
|
277
|
+
/*
|
|
278
|
+
* Always run the ContextSpanProcessor so the ambient TelemetryContext
|
|
279
|
+
* attributes (projectId, userId, monitorId, incidentId, requestId, ...)
|
|
280
|
+
* are stamped onto every span at creation. The BatchSpanProcessor that
|
|
281
|
+
* actually exports spans is added after it, and only when an exporter is
|
|
282
|
+
* configured. (traceExporter is deprecated in favour of spanProcessors.)
|
|
283
|
+
*/
|
|
284
|
+
const spanProcessors: Array<SpanProcessor> = [new ContextSpanProcessor()];
|
|
285
|
+
|
|
271
286
|
if (traceExporter) {
|
|
272
|
-
|
|
287
|
+
spanProcessors.push(new BatchSpanProcessor(traceExporter));
|
|
273
288
|
}
|
|
274
289
|
|
|
290
|
+
nodeSdkConfiguration.spanProcessors = spanProcessors;
|
|
291
|
+
|
|
275
292
|
/*
|
|
276
293
|
* We will skip this becasue we're attachng this metric reader to the meter provider later.
|
|
277
294
|
* if (this.metricReader) {
|
|
@@ -294,11 +311,20 @@ export default class Telemetry {
|
|
|
294
311
|
this.getMeterProvider();
|
|
295
312
|
this.getMeter();
|
|
296
313
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
314
|
+
/*
|
|
315
|
+
* Flush traces / metrics / logs last (Telemetry tier) so spans and logs
|
|
316
|
+
* emitted by the rest of the shutdown still get exported. GracefulShutdown
|
|
317
|
+
* owns process.exit now — this handler must NOT call it itself, or it
|
|
318
|
+
* would race the other tiers and abandon the datastore pools (the exact
|
|
319
|
+
* bug this replaced).
|
|
320
|
+
*/
|
|
321
|
+
GracefulShutdown.registerHandler(
|
|
322
|
+
"Telemetry",
|
|
323
|
+
ShutdownPriority.Telemetry,
|
|
324
|
+
() => {
|
|
325
|
+
return sdk.shutdown();
|
|
326
|
+
},
|
|
327
|
+
);
|
|
302
328
|
|
|
303
329
|
sdk.start();
|
|
304
330
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { TelemetryAttributeService } from "../../../Server/Services/TelemetryAttributeService";
|
|
2
|
+
import { Statement } from "../../../Server/Utils/AnalyticsDatabase/Statement";
|
|
3
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
4
|
+
import { describe, expect, test } from "@jest/globals";
|
|
5
|
+
|
|
6
|
+
describe("TelemetryAttributeService.buildAttributeValuesStatement", () => {
|
|
7
|
+
/*
|
|
8
|
+
* Only the column/table names are read while building the statement, so a
|
|
9
|
+
* lightweight source literal is enough — no real AnalyticsDatabaseService.
|
|
10
|
+
*/
|
|
11
|
+
const source: unknown = {
|
|
12
|
+
tableName: "MetricItemV2",
|
|
13
|
+
attributesColumn: "attributes",
|
|
14
|
+
attributeKeysColumn: "attributeKeys",
|
|
15
|
+
timeColumn: "time",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type BuildInput = {
|
|
19
|
+
projectId: ObjectID;
|
|
20
|
+
source: unknown;
|
|
21
|
+
metricName?: string | undefined;
|
|
22
|
+
attributeKey: string;
|
|
23
|
+
searchText?: string | undefined;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const buildValuesStatement: (overrides?: Partial<BuildInput>) => Statement = (
|
|
27
|
+
overrides: Partial<BuildInput> = {},
|
|
28
|
+
): Statement => {
|
|
29
|
+
return (
|
|
30
|
+
TelemetryAttributeService as unknown as {
|
|
31
|
+
buildAttributeValuesStatement: (data: BuildInput) => Statement;
|
|
32
|
+
}
|
|
33
|
+
).buildAttributeValuesStatement({
|
|
34
|
+
projectId: ObjectID.generate(),
|
|
35
|
+
source,
|
|
36
|
+
attributeKey: "host.name",
|
|
37
|
+
...overrides,
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
test("omits the ILIKE filter when no search text is provided", () => {
|
|
42
|
+
const statement: Statement = buildValuesStatement();
|
|
43
|
+
|
|
44
|
+
expect(statement.query).not.toContain("ILIKE");
|
|
45
|
+
expect(statement.query).toContain("ORDER BY attributeValue ASC");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("omits the ILIKE filter when search text is only whitespace", () => {
|
|
49
|
+
const statement: Statement = buildValuesStatement({ searchText: " " });
|
|
50
|
+
|
|
51
|
+
expect(statement.query).not.toContain("ILIKE");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("adds a case-insensitive substring filter when search text is provided", () => {
|
|
55
|
+
const statement: Statement = buildValuesStatement({ searchText: "web" });
|
|
56
|
+
|
|
57
|
+
expect(statement.query).toContain("ILIKE");
|
|
58
|
+
// The value is parameterized and wrapped with % wildcards.
|
|
59
|
+
expect(Object.values(statement.query_params)).toContain("%web%");
|
|
60
|
+
// The attribute key is always parameterized — never inlined into SQL.
|
|
61
|
+
expect(Object.values(statement.query_params)).toContain("host.name");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("trims surrounding whitespace from the search text", () => {
|
|
65
|
+
const statement: Statement = buildValuesStatement({
|
|
66
|
+
searchText: " web-server ",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(Object.values(statement.query_params)).toContain("%web-server%");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("scopes to a metric when metricName is provided", () => {
|
|
73
|
+
const statement: Statement = buildValuesStatement({
|
|
74
|
+
metricName: "http.server.duration",
|
|
75
|
+
searchText: "web",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(statement.query).toContain("AND name =");
|
|
79
|
+
expect(Object.values(statement.query_params)).toContain(
|
|
80
|
+
"http.server.duration",
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
Binary file
|
|
@@ -2,11 +2,39 @@ import GenericFunction from "../../GenericFunction";
|
|
|
2
2
|
|
|
3
3
|
export interface OwnedThroughMetadata {
|
|
4
4
|
fkColumn: string;
|
|
5
|
-
|
|
5
|
+
/*
|
|
6
|
+
* The parent resource type(s) whose ownership grants access to this
|
|
7
|
+
* model's rows. Usually one (e.g. Monitor), but a polymorphic FK can
|
|
8
|
+
* inherit ownership from several resource types — e.g. a telemetry
|
|
9
|
+
* row's serviceId may reference a Service, Host, DockerHost or
|
|
10
|
+
* KubernetesCluster, so Owned scope unions the owned ids across all of
|
|
11
|
+
* them.
|
|
12
|
+
*/
|
|
13
|
+
parentModels: Array<GenericFunction>;
|
|
14
|
+
/*
|
|
15
|
+
* When true, rows whose fkColumn equals the tenant (project) id itself
|
|
16
|
+
* are also visible under Owned scope. Telemetry with no owning resource
|
|
17
|
+
* (the unattributed "Unknown" bucket) is tagged with the projectId in
|
|
18
|
+
* place of a resource id; it belongs to the project, not any single
|
|
19
|
+
* owner, so every in-project user with the table permission may see it.
|
|
20
|
+
*/
|
|
21
|
+
includeProjectScope: boolean;
|
|
6
22
|
}
|
|
7
23
|
|
|
8
|
-
export
|
|
24
|
+
export interface OwnedThroughOptions {
|
|
25
|
+
includeProjectScope?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default (
|
|
29
|
+
fkColumn: string,
|
|
30
|
+
parentModel: GenericFunction | Array<GenericFunction>,
|
|
31
|
+
options?: OwnedThroughOptions,
|
|
32
|
+
) => {
|
|
9
33
|
return (ctr: GenericFunction) => {
|
|
10
|
-
ctr.prototype.ownedThrough = {
|
|
34
|
+
ctr.prototype.ownedThrough = {
|
|
35
|
+
fkColumn,
|
|
36
|
+
parentModels: Array.isArray(parentModel) ? parentModel : [parentModel],
|
|
37
|
+
includeProjectScope: options?.includeProjectScope ?? false,
|
|
38
|
+
};
|
|
11
39
|
};
|
|
12
40
|
};
|
|
@@ -14,6 +14,16 @@ enum ServiceType {
|
|
|
14
14
|
Host = "Host",
|
|
15
15
|
DockerHost = "DockerHost",
|
|
16
16
|
KubernetesCluster = "KubernetesCluster",
|
|
17
|
+
/*
|
|
18
|
+
* Telemetry that arrived without an OTel service.name and with no
|
|
19
|
+
* host / docker / k8s resource signal. Instead of synthesising a
|
|
20
|
+
* placeholder "Unknown Service" Postgres row (which collected every
|
|
21
|
+
* oneuptime.label.* attribute from unrelated sources), the row's
|
|
22
|
+
* `serviceId` slot holds the projectId and no Service row is created.
|
|
23
|
+
* The read side renders these under a synthetic "Unknown Service"
|
|
24
|
+
* bucket.
|
|
25
|
+
*/
|
|
26
|
+
Unknown = "Unknown",
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
export default ServiceType;
|
|
@@ -29,7 +29,13 @@ export interface ComponentProps {
|
|
|
29
29
|
const BASE_INPUT_CLASS: string =
|
|
30
30
|
"block w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-3 text-sm placeholder-gray-500 focus:border-indigo-500 focus:text-gray-900 focus:placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm";
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
/*
|
|
33
|
+
* Aligned with the server-side attribute-value limit (ATTRIBUTE_VALUES_LIMIT)
|
|
34
|
+
* so that when suggestions are fetched server-side (and already narrowed by
|
|
35
|
+
* search text), the dropdown can show the full returned set rather than
|
|
36
|
+
* truncating it further on the client.
|
|
37
|
+
*/
|
|
38
|
+
const MAX_SUGGESTIONS: number = 100;
|
|
33
39
|
|
|
34
40
|
// Provides a free-form text input with an optional suggestion dropdown.
|
|
35
41
|
const AutocompleteTextInput: FunctionComponent<ComponentProps> = (
|
|
@@ -39,6 +39,12 @@ export interface ComponentProps {
|
|
|
39
39
|
onKeySelected?: ((key: string) => void) | undefined;
|
|
40
40
|
isLoadingKeys?: boolean | undefined;
|
|
41
41
|
loadingValueKeys?: Array<string> | undefined;
|
|
42
|
+
/*
|
|
43
|
+
* Called (with the row's key and the current value text) as the user
|
|
44
|
+
* types in the value input, so the parent can fetch refined value
|
|
45
|
+
* suggestions server-side. Debouncing is the parent's responsibility.
|
|
46
|
+
*/
|
|
47
|
+
onValueSearch?: ((key: string, searchText: string) => void) | undefined;
|
|
42
48
|
/*
|
|
43
49
|
* When true, render an operator dropdown (=, !=, contains, etc.)
|
|
44
50
|
* between the key and value inputs. Defaults to false for backwards
|
|
@@ -393,6 +399,19 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
|
|
|
393
399
|
newData[index]!.value = value;
|
|
394
400
|
setData(newData);
|
|
395
401
|
onDataChange(newData);
|
|
402
|
+
|
|
403
|
+
/*
|
|
404
|
+
* Let the parent refine value suggestions server-side
|
|
405
|
+
* as the user types. Skip numeric operators — those
|
|
406
|
+
* have no value suggestions to narrow.
|
|
407
|
+
*/
|
|
408
|
+
if (
|
|
409
|
+
props.onValueSearch &&
|
|
410
|
+
item.key &&
|
|
411
|
+
!operatorOption.expectsNumericValue
|
|
412
|
+
) {
|
|
413
|
+
props.onValueSearch(item.key, value);
|
|
414
|
+
}
|
|
396
415
|
}}
|
|
397
416
|
/>
|
|
398
417
|
)}
|
|
@@ -186,6 +186,7 @@ const FiltersForm: FiltersFormFunction = <T extends GenericObject>(
|
|
|
186
186
|
onJsonKeySelected={filter.onJsonKeySelected}
|
|
187
187
|
isLoadingJsonKeys={filter.isLoadingJsonKeys}
|
|
188
188
|
loadingJsonValueKeys={filter.loadingJsonValueKeys}
|
|
189
|
+
onJsonValueSearch={filter.onJsonValueSearch}
|
|
189
190
|
enableOperators={filter.jsonEnableOperators}
|
|
190
191
|
/>
|
|
191
192
|
</div>
|
|
@@ -17,6 +17,7 @@ export interface ComponentProps<T extends GenericObject> {
|
|
|
17
17
|
onJsonKeySelected?: ((key: string) => void) | undefined;
|
|
18
18
|
isLoadingJsonKeys?: boolean | undefined;
|
|
19
19
|
loadingJsonValueKeys?: Array<string> | undefined;
|
|
20
|
+
onJsonValueSearch?: ((key: string, searchText: string) => void) | undefined;
|
|
20
21
|
enableOperators?: boolean | undefined;
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -38,6 +39,7 @@ const JSONFilter: JSONFilterFunction = <T extends GenericObject>(
|
|
|
38
39
|
onKeySelected={props.onJsonKeySelected}
|
|
39
40
|
isLoadingKeys={props.isLoadingJsonKeys}
|
|
40
41
|
loadingValueKeys={props.loadingJsonValueKeys}
|
|
42
|
+
onValueSearch={props.onJsonValueSearch}
|
|
41
43
|
enableOperators={props.enableOperators}
|
|
42
44
|
addButtonSuffix={filter.title}
|
|
43
45
|
keyPlaceholder={"Key"}
|
|
@@ -12,6 +12,7 @@ export default interface Filter<T extends GenericObject> {
|
|
|
12
12
|
onJsonKeySelected?: ((key: string) => void) | undefined;
|
|
13
13
|
isLoadingJsonKeys?: boolean | undefined;
|
|
14
14
|
loadingJsonValueKeys?: Array<string> | undefined;
|
|
15
|
+
onJsonValueSearch?: ((key: string, searchText: string) => void) | undefined;
|
|
15
16
|
jsonEnableOperators?: boolean | undefined;
|
|
16
17
|
isAdvancedFilter?: boolean | undefined;
|
|
17
18
|
}
|
|
@@ -60,6 +60,8 @@ import { queryStringToFilter } from "../../../Types/Log/LogQueryToFilter";
|
|
|
60
60
|
import RangeStartAndEndDateTime from "../../../Types/Time/RangeStartAndEndDateTime";
|
|
61
61
|
import TimeRange from "../../../Types/Time/TimeRange";
|
|
62
62
|
import { exportLogs, LogExportFormat } from "../../Utils/LogExport";
|
|
63
|
+
import ProjectUtil from "../../Utils/Project";
|
|
64
|
+
import TelemetryServiceUtil from "../../Utils/TelemetryService";
|
|
63
65
|
import ObjectID from "../../../Types/ObjectID";
|
|
64
66
|
import OneUptimeDate from "../../../Types/Date";
|
|
65
67
|
|
|
@@ -460,6 +462,20 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
460
462
|
services[service.id.toString()] = service;
|
|
461
463
|
});
|
|
462
464
|
|
|
465
|
+
/*
|
|
466
|
+
* Logs without a service.name are tagged with the projectId
|
|
467
|
+
* (ServiceType.Unknown) and have no Service row. Register a
|
|
468
|
+
* synthetic "Unknown Service" keyed by the projectId so the
|
|
469
|
+
* serviceId -> name resolution and the serviceId facet render
|
|
470
|
+
* "Unknown Service" instead of a raw id. Harmless when no logs
|
|
471
|
+
* are unattributed — nothing resolves against it.
|
|
472
|
+
*/
|
|
473
|
+
const projectId: ObjectID | null = ProjectUtil.getCurrentProjectId();
|
|
474
|
+
if (projectId) {
|
|
475
|
+
services[projectId.toString()] =
|
|
476
|
+
TelemetryServiceUtil.getUnknownService(projectId);
|
|
477
|
+
}
|
|
478
|
+
|
|
463
479
|
const hostsById: Dictionary<Host> = {};
|
|
464
480
|
hosts.data.forEach((host: Host) => {
|
|
465
481
|
if (!host.id) {
|
package/UI/Utils/Project.ts
CHANGED
|
@@ -12,6 +12,7 @@ import SubscriptionStatus, {
|
|
|
12
12
|
} from "../../Types/Billing/SubscriptionStatus";
|
|
13
13
|
import Navigation from "./Navigation";
|
|
14
14
|
import SessionStorage from "./SessionStorage";
|
|
15
|
+
import Telemetry from "./Telemetry/Telemetry";
|
|
15
16
|
|
|
16
17
|
export default class ProjectUtil {
|
|
17
18
|
public static getCurrentProjectId(): ObjectID | null {
|
|
@@ -107,6 +108,11 @@ export default class ProjectUtil {
|
|
|
107
108
|
}
|
|
108
109
|
LocalStorage.setItem(`project_${currentProjectId}`, project);
|
|
109
110
|
SessionStorage.setItem(`current_project_id`, currentProjectId);
|
|
111
|
+
|
|
112
|
+
// Keep RUM span context in sync with the project being viewed.
|
|
113
|
+
if (currentProjectId) {
|
|
114
|
+
Telemetry.setGlobalAttributes({ projectId: currentProjectId });
|
|
115
|
+
}
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
public static clearCurrentProject(): void {
|
|
@@ -19,7 +19,65 @@ import type { SpanExporter as WebSpanExporter } from "@opentelemetry/sdk-trace-w
|
|
|
19
19
|
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
20
20
|
import URL from "../../../Types/API/URL";
|
|
21
21
|
|
|
22
|
+
/*
|
|
23
|
+
* Mutable, module-level bag of attributes stamped onto every browser span.
|
|
24
|
+
* Populated by `Telemetry.setGlobalAttributes` once the signed-in user and
|
|
25
|
+
* the project being viewed are known (RUM context).
|
|
26
|
+
*/
|
|
27
|
+
const globalSpanAttributes: { [key: string]: string } = {};
|
|
28
|
+
|
|
29
|
+
interface MutableSpanLike {
|
|
30
|
+
setAttribute(key: string, value: string): unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Stamps the global RUM attributes (userId, projectId, ...) onto each span at
|
|
35
|
+
* creation, mirroring the server-side ContextSpanProcessor.
|
|
36
|
+
*/
|
|
37
|
+
class GlobalAttributeSpanProcessor {
|
|
38
|
+
public onStart(span: MutableSpanLike): void {
|
|
39
|
+
for (const key in globalSpanAttributes) {
|
|
40
|
+
const value: string | undefined = globalSpanAttributes[key];
|
|
41
|
+
|
|
42
|
+
if (value) {
|
|
43
|
+
span.setAttribute(key, value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public onEnd(): void {
|
|
49
|
+
// no-op
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public forceFlush(): Promise<void> {
|
|
53
|
+
return Promise.resolve();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public shutdown(): Promise<void> {
|
|
57
|
+
return Promise.resolve();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
22
61
|
export default class Telemetry {
|
|
62
|
+
/**
|
|
63
|
+
* Set RUM attributes that should be attached to every browser span from now
|
|
64
|
+
* on (e.g. userId, projectId). Safe to call before or after `init`; empty
|
|
65
|
+
* values are ignored.
|
|
66
|
+
*/
|
|
67
|
+
public static setGlobalAttributes(attributes: {
|
|
68
|
+
userId?: string | undefined;
|
|
69
|
+
projectId?: string | undefined;
|
|
70
|
+
[key: string]: string | undefined;
|
|
71
|
+
}): void {
|
|
72
|
+
for (const key in attributes) {
|
|
73
|
+
const value: string | undefined = attributes[key];
|
|
74
|
+
|
|
75
|
+
if (value) {
|
|
76
|
+
globalSpanAttributes[key] = value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
23
81
|
public static init(data: { serviceName: string }): void {
|
|
24
82
|
if (DisableTelemetry) {
|
|
25
83
|
return;
|
|
@@ -49,6 +107,13 @@ export default class Telemetry {
|
|
|
49
107
|
|
|
50
108
|
provider.addSpanProcessor(new BatchSpanProcessor(webTraceExporter));
|
|
51
109
|
|
|
110
|
+
// Stamp global RUM attributes (userId, projectId, ...) onto every span.
|
|
111
|
+
provider.addSpanProcessor(
|
|
112
|
+
new GlobalAttributeSpanProcessor() as unknown as Parameters<
|
|
113
|
+
WebTracerProvider["addSpanProcessor"]
|
|
114
|
+
>[0],
|
|
115
|
+
);
|
|
116
|
+
|
|
52
117
|
provider.register({
|
|
53
118
|
contextManager: new ZoneContextManager(),
|
|
54
119
|
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import Service from "../../Models/DatabaseModels/Service";
|
|
2
|
+
import { Gray500 } from "../../Types/BrandColors";
|
|
3
|
+
import ObjectID from "../../Types/ObjectID";
|
|
4
|
+
import ServiceType from "../../Types/Telemetry/ServiceType";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* Telemetry that arrives without an OTel service.name (and with no
|
|
8
|
+
* host / docker / k8s resource signal) is not backed by a Service row.
|
|
9
|
+
* The ingest path tags those analytics rows with the projectId in the
|
|
10
|
+
* `serviceId` column under ServiceType.Unknown (see
|
|
11
|
+
* OtelIngestBaseService.resolveTelemetryResource). The read side has no
|
|
12
|
+
* Service to resolve, so we represent that telemetry with a synthetic,
|
|
13
|
+
* non-persisted Service whose id is the projectId — that way the
|
|
14
|
+
* existing serviceId -> Service lookups in the telemetry views resolve
|
|
15
|
+
* it to a labelled "Unknown Service" entry without any per-view special
|
|
16
|
+
* casing. A real Service._id is never equal to a projectId, so this
|
|
17
|
+
* never collides with a genuine service.
|
|
18
|
+
*/
|
|
19
|
+
export const UNKNOWN_SERVICE_NAME: string = "Unknown Service";
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
* Result of resolving a telemetry row's polymorphic (serviceId,
|
|
23
|
+
* serviceType) to something renderable. Either a Service (a real
|
|
24
|
+
* OpenTelemetry service, or the synthetic "Unknown Service" for the
|
|
25
|
+
* unattributed bucket) — or a plain `label` for infrastructure resource
|
|
26
|
+
* types (Host / DockerHost / KubernetesCluster) that have no Service row.
|
|
27
|
+
*/
|
|
28
|
+
export interface ResolvedTelemetryResource {
|
|
29
|
+
service?: Service;
|
|
30
|
+
label?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default class TelemetryServiceUtil {
|
|
34
|
+
/*
|
|
35
|
+
* True when a telemetry row's serviceId is the synthetic "Unknown
|
|
36
|
+
* Service" — i.e. it equals the projectId (ServiceType.Unknown). Used
|
|
37
|
+
* to suppress navigation to a per-service detail page that does not
|
|
38
|
+
* exist for unattributed telemetry.
|
|
39
|
+
*/
|
|
40
|
+
public static isUnknownServiceId(
|
|
41
|
+
serviceId: ObjectID | string | null | undefined,
|
|
42
|
+
projectId: ObjectID | null | undefined,
|
|
43
|
+
): boolean {
|
|
44
|
+
if (!serviceId || !projectId) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return serviceId.toString() === projectId.toString();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/*
|
|
51
|
+
* Build the synthetic Service used to render unattributed telemetry.
|
|
52
|
+
* Not persisted — id is the projectId so that serviceId -> Service
|
|
53
|
+
* lookups (which key on the analytics row's serviceId) resolve to it.
|
|
54
|
+
*/
|
|
55
|
+
public static getUnknownService(projectId: ObjectID): Service {
|
|
56
|
+
const service: Service = new Service();
|
|
57
|
+
service.id = projectId;
|
|
58
|
+
service.name = UNKNOWN_SERVICE_NAME;
|
|
59
|
+
service.serviceColor = Gray500;
|
|
60
|
+
return service;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/*
|
|
64
|
+
* Resolve a telemetry row's polymorphic (serviceId, serviceType) to a
|
|
65
|
+
* renderable resource, given the project's loaded Services. Replaces the
|
|
66
|
+
* old server-side `service` ORM relation on TelemetryException: a real
|
|
67
|
+
* Service resolves from the loaded list, the unattributed bucket resolves
|
|
68
|
+
* to the synthetic "Unknown Service", and Host / DockerHost /
|
|
69
|
+
* KubernetesCluster resolve to a type label (no Service row exists for
|
|
70
|
+
* them). Mirrors how the ClickHouse analytics rows are resolved.
|
|
71
|
+
*/
|
|
72
|
+
public static resolveTelemetryResource(data: {
|
|
73
|
+
serviceId: ObjectID | string | null | undefined;
|
|
74
|
+
serviceType: ServiceType | string | null | undefined;
|
|
75
|
+
services: Array<Service>;
|
|
76
|
+
projectId: ObjectID | null | undefined;
|
|
77
|
+
}): ResolvedTelemetryResource {
|
|
78
|
+
const serviceIdStr: string | undefined = data.serviceId?.toString();
|
|
79
|
+
|
|
80
|
+
// Real Service (OpenTelemetry) — resolve from the loaded list.
|
|
81
|
+
if (serviceIdStr) {
|
|
82
|
+
const found: Service | undefined = data.services.find((s: Service) => {
|
|
83
|
+
return s.id?.toString() === serviceIdStr;
|
|
84
|
+
});
|
|
85
|
+
if (found) {
|
|
86
|
+
return { service: found };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Unattributed (Unknown) bucket — serviceId is the projectId.
|
|
91
|
+
if (
|
|
92
|
+
data.projectId &&
|
|
93
|
+
(data.serviceType === ServiceType.Unknown ||
|
|
94
|
+
this.isUnknownServiceId(data.serviceId, data.projectId))
|
|
95
|
+
) {
|
|
96
|
+
return { service: this.getUnknownService(data.projectId) };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Infrastructure resource types — no Service row; render a type label.
|
|
100
|
+
const typeLabels: Record<string, string> = {
|
|
101
|
+
[ServiceType.Host]: "Host telemetry",
|
|
102
|
+
[ServiceType.DockerHost]: "Docker host telemetry",
|
|
103
|
+
[ServiceType.KubernetesCluster]: "Kubernetes telemetry",
|
|
104
|
+
};
|
|
105
|
+
const label: string | undefined = data.serviceType
|
|
106
|
+
? typeLabels[data.serviceType.toString()]
|
|
107
|
+
: undefined;
|
|
108
|
+
if (label) {
|
|
109
|
+
return { label: label };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { label: "Unknown" };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/*
|
|
116
|
+
* Append the synthetic "Unknown Service" to a loaded service list, but
|
|
117
|
+
* only when the telemetry in view actually references it (some
|
|
118
|
+
* serviceId equals the projectId). Avoids showing an empty "Unknown
|
|
119
|
+
* Service" entry for projects that always set service.name. Idempotent.
|
|
120
|
+
*/
|
|
121
|
+
public static withUnknownServiceIfReferenced(data: {
|
|
122
|
+
services: Array<Service>;
|
|
123
|
+
referencedServiceIds: Iterable<string>;
|
|
124
|
+
projectId: ObjectID;
|
|
125
|
+
}): Array<Service> {
|
|
126
|
+
const projectIdStr: string = data.projectId.toString();
|
|
127
|
+
|
|
128
|
+
let isReferenced: boolean = false;
|
|
129
|
+
for (const id of data.referencedServiceIds) {
|
|
130
|
+
if (id === projectIdStr) {
|
|
131
|
+
isReferenced = true;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!isReferenced) {
|
|
137
|
+
return data.services;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const alreadyPresent: boolean = data.services.some((service: Service) => {
|
|
141
|
+
return service.id?.toString() === projectIdStr;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (alreadyPresent) {
|
|
145
|
+
return data.services;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return [...data.services, this.getUnknownService(data.projectId)];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -785,7 +785,7 @@ let ExceptionInstance = class ExceptionInstance extends AnalyticsBaseModel {
|
|
|
785
785
|
};
|
|
786
786
|
ExceptionInstance = __decorate([
|
|
787
787
|
OperationalResource(),
|
|
788
|
-
OwnedThrough("serviceId", Service),
|
|
788
|
+
OwnedThrough("serviceId", Service, { includeProjectScope: true }),
|
|
789
789
|
__metadata("design:paramtypes", [])
|
|
790
790
|
], ExceptionInstance);
|
|
791
791
|
export default ExceptionInstance;
|