@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.
Files changed (164) hide show
  1. package/Models/AnalyticsModels/ExceptionInstance.ts +1 -1
  2. package/Models/AnalyticsModels/Log.ts +1 -1
  3. package/Models/AnalyticsModels/Metric.ts +1 -1
  4. package/Models/AnalyticsModels/Profile.ts +1 -1
  5. package/Models/AnalyticsModels/ProfileSample.ts +1 -1
  6. package/Models/AnalyticsModels/Span.ts +1 -1
  7. package/Models/DatabaseModels/TelemetryException.ts +46 -34
  8. package/Models/DatabaseModels/TelemetryUsageBilling.ts +35 -2
  9. package/Server/API/AIAgentDataAPI.ts +25 -7
  10. package/Server/API/TelemetryAPI.ts +6 -0
  11. package/Server/API/TelemetryExceptionAPI.ts +6 -2
  12. package/Server/EnvironmentConfig.ts +27 -0
  13. package/Server/Infrastructure/ClickhouseDatabase.ts +21 -1
  14. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +19 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.ts +28 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.ts +24 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.ts +47 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.ts +34 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  20. package/Server/Infrastructure/PostgresDatabase.ts +27 -1
  21. package/Server/Infrastructure/QueueWorker.ts +54 -4
  22. package/Server/Infrastructure/Redis.ts +11 -0
  23. package/Server/Services/AnalyticsDatabaseService.ts +87 -0
  24. package/Server/Services/DatabaseService.ts +73 -0
  25. package/Server/Services/TelemetryAttributeService.ts +38 -2
  26. package/Server/Services/TelemetryExceptionService.ts +24 -49
  27. package/Server/Services/TelemetryUsageBillingService.ts +289 -166
  28. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +102 -72
  29. package/Server/Types/Database/Permissions/OwnedScopePermission.ts +81 -60
  30. package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +67 -0
  31. package/Server/Utils/Express.ts +32 -0
  32. package/Server/Utils/GracefulShutdown.ts +194 -0
  33. package/Server/Utils/Logger.ts +12 -1
  34. package/Server/Utils/Monitor/MonitorLogUtil.ts +22 -17
  35. package/Server/Utils/Profiling.ts +14 -6
  36. package/Server/Utils/StartServer.ts +13 -5
  37. package/Server/Utils/Telemetry/ContextSpanProcessor.ts +48 -0
  38. package/Server/Utils/Telemetry/LogExceptionExtractor.ts +289 -0
  39. package/Server/Utils/Telemetry/SpanUtil.ts +16 -35
  40. package/Server/Utils/Telemetry/StackTraceParser.ts +423 -0
  41. package/Server/Utils/Telemetry/TelemetryContext.ts +190 -0
  42. package/Server/Utils/Telemetry.ts +33 -7
  43. package/Tests/Server/Services/TelemetryAttributeService.test.ts +83 -0
  44. package/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.ts +0 -0
  45. package/Types/Database/AccessControl/OwnedThrough.ts +31 -3
  46. package/Types/Telemetry/ServiceType.ts +10 -0
  47. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +7 -1
  48. package/UI/Components/Dictionary/Dictionary.tsx +19 -0
  49. package/UI/Components/Filters/FiltersForm.tsx +1 -0
  50. package/UI/Components/Filters/JSONFilter.tsx +2 -0
  51. package/UI/Components/Filters/Types/Filter.ts +1 -0
  52. package/UI/Components/LogsViewer/LogsViewer.tsx +16 -0
  53. package/UI/Utils/Project.ts +6 -0
  54. package/UI/Utils/Telemetry/Telemetry.ts +65 -0
  55. package/UI/Utils/TelemetryService.ts +150 -0
  56. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +1 -1
  57. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
  58. package/build/dist/Models/AnalyticsModels/Log.js +1 -1
  59. package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
  60. package/build/dist/Models/AnalyticsModels/Metric.js +1 -1
  61. package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
  62. package/build/dist/Models/AnalyticsModels/Profile.js +1 -1
  63. package/build/dist/Models/AnalyticsModels/Profile.js.map +1 -1
  64. package/build/dist/Models/AnalyticsModels/ProfileSample.js +1 -1
  65. package/build/dist/Models/AnalyticsModels/ProfileSample.js.map +1 -1
  66. package/build/dist/Models/AnalyticsModels/Span.js +1 -1
  67. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  68. package/build/dist/Models/DatabaseModels/TelemetryException.js +47 -33
  69. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  70. package/build/dist/Models/DatabaseModels/TelemetryUsageBilling.js +36 -2
  71. package/build/dist/Models/DatabaseModels/TelemetryUsageBilling.js.map +1 -1
  72. package/build/dist/Server/API/AIAgentDataAPI.js +24 -8
  73. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  74. package/build/dist/Server/API/TelemetryAPI.js +4 -0
  75. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  76. package/build/dist/Server/API/TelemetryExceptionAPI.js +6 -2
  77. package/build/dist/Server/API/TelemetryExceptionAPI.js.map +1 -1
  78. package/build/dist/Server/EnvironmentConfig.js +19 -0
  79. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  80. package/build/dist/Server/Infrastructure/ClickhouseDatabase.js +16 -2
  81. package/build/dist/Server/Infrastructure/ClickhouseDatabase.js.map +1 -1
  82. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +10 -9
  83. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  84. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.js +23 -0
  85. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.js.map +1 -0
  86. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.js +19 -0
  87. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.js.map +1 -0
  88. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.js +22 -0
  89. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.js.map +1 -0
  90. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.js +25 -0
  91. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.js.map +1 -0
  92. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  93. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  94. package/build/dist/Server/Infrastructure/PostgresDatabase.js +20 -1
  95. package/build/dist/Server/Infrastructure/PostgresDatabase.js.map +1 -1
  96. package/build/dist/Server/Infrastructure/QueueWorker.js +40 -3
  97. package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
  98. package/build/dist/Server/Infrastructure/Redis.js +5 -0
  99. package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
  100. package/build/dist/Server/Services/AnalyticsDatabaseService.js +59 -0
  101. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  102. package/build/dist/Server/Services/DatabaseService.js +62 -0
  103. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  104. package/build/dist/Server/Services/TelemetryAttributeService.js +23 -1
  105. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  106. package/build/dist/Server/Services/TelemetryExceptionService.js +16 -41
  107. package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
  108. package/build/dist/Server/Services/TelemetryUsageBillingService.js +211 -147
  109. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  110. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +84 -63
  111. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  112. package/build/dist/Server/Types/Database/Permissions/OwnedScopePermission.js +67 -49
  113. package/build/dist/Server/Types/Database/Permissions/OwnedScopePermission.js.map +1 -1
  114. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +51 -0
  115. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
  116. package/build/dist/Server/Utils/Express.js +23 -0
  117. package/build/dist/Server/Utils/Express.js.map +1 -1
  118. package/build/dist/Server/Utils/GracefulShutdown.js +145 -0
  119. package/build/dist/Server/Utils/GracefulShutdown.js.map +1 -0
  120. package/build/dist/Server/Utils/Logger.js +8 -1
  121. package/build/dist/Server/Utils/Logger.js.map +1 -1
  122. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -10
  123. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
  124. package/build/dist/Server/Utils/Profiling.js +8 -3
  125. package/build/dist/Server/Utils/Profiling.js.map +1 -1
  126. package/build/dist/Server/Utils/StartServer.js +12 -4
  127. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  128. package/build/dist/Server/Utils/Telemetry/ContextSpanProcessor.js +37 -0
  129. package/build/dist/Server/Utils/Telemetry/ContextSpanProcessor.js.map +1 -0
  130. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js +214 -0
  131. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js.map +1 -0
  132. package/build/dist/Server/Utils/Telemetry/SpanUtil.js +15 -24
  133. package/build/dist/Server/Utils/Telemetry/SpanUtil.js.map +1 -1
  134. package/build/dist/Server/Utils/Telemetry/StackTraceParser.js +365 -0
  135. package/build/dist/Server/Utils/Telemetry/StackTraceParser.js.map +1 -0
  136. package/build/dist/Server/Utils/Telemetry/TelemetryContext.js +124 -0
  137. package/build/dist/Server/Utils/Telemetry/TelemetryContext.js.map +1 -0
  138. package/build/dist/Server/Utils/Telemetry.js +22 -5
  139. package/build/dist/Server/Utils/Telemetry.js.map +1 -1
  140. package/build/dist/Tests/Server/Services/TelemetryAttributeService.test.js +50 -0
  141. package/build/dist/Tests/Server/Services/TelemetryAttributeService.test.js.map +1 -0
  142. package/build/dist/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.js +0 -0
  143. package/build/dist/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.js.map +1 -0
  144. package/build/dist/Types/Database/AccessControl/OwnedThrough.js +7 -2
  145. package/build/dist/Types/Database/AccessControl/OwnedThrough.js.map +1 -1
  146. package/build/dist/Types/Telemetry/ServiceType.js +10 -0
  147. package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
  148. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +7 -1
  149. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  150. package/build/dist/UI/Components/Dictionary/Dictionary.js +10 -0
  151. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  152. package/build/dist/UI/Components/Filters/FiltersForm.js +1 -1
  153. package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
  154. package/build/dist/UI/Components/Filters/JSONFilter.js +1 -1
  155. package/build/dist/UI/Components/Filters/JSONFilter.js.map +1 -1
  156. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +15 -0
  157. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  158. package/build/dist/UI/Utils/Project.js +5 -0
  159. package/build/dist/UI/Utils/Project.js.map +1 -1
  160. package/build/dist/UI/Utils/Telemetry/Telemetry.js +44 -0
  161. package/build/dist/UI/Utils/Telemetry/Telemetry.js.map +1 -1
  162. package/build/dist/UI/Utils/TelemetryService.js +113 -0
  163. package/build/dist/UI/Utils/TelemetryService.js.map +1 -0
  164. 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 { SpanExporter } from "@opentelemetry/sdk-trace-base";
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
- nodeSdkConfiguration.traceExporter = traceExporter;
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
- process.on("SIGTERM", () => {
298
- sdk.shutdown().finally(() => {
299
- return process.exit(0);
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
+ });
@@ -2,11 +2,39 @@ import GenericFunction from "../../GenericFunction";
2
2
 
3
3
  export interface OwnedThroughMetadata {
4
4
  fkColumn: string;
5
- parentModel: GenericFunction;
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 default (fkColumn: string, parentModel: GenericFunction) => {
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 = { fkColumn, parentModel };
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
- const MAX_SUGGESTIONS: number = 50;
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) {
@@ -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;