@oneuptime/common 10.0.86 → 10.0.89

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 (126) hide show
  1. package/Models/DatabaseModels/EnterpriseLicense.ts +54 -0
  2. package/Models/DatabaseModels/GlobalConfig.ts +51 -0
  3. package/Server/API/EnterpriseLicenseAPI.ts +83 -0
  4. package/Server/API/GlobalConfigAPI.ts +59 -0
  5. package/Server/API/MetricAPI.ts +149 -0
  6. package/Server/API/TelemetryAPI.ts +24 -0
  7. package/Server/EnvironmentConfig.ts +10 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.ts +59 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  10. package/Server/Infrastructure/Queue.ts +4 -4
  11. package/Server/Services/AnalyticsDatabaseService.ts +21 -0
  12. package/Server/Services/MetricService.ts +193 -1
  13. package/Server/Services/TelemetryAttributeService.ts +37 -3
  14. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +174 -7
  15. package/Tests/Types/Date.test.ts +46 -0
  16. package/Types/Dashboard/DashboardComponentType.ts +3 -0
  17. package/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.ts +13 -0
  18. package/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.ts +13 -0
  19. package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +13 -0
  20. package/Types/Date.ts +9 -4
  21. package/Types/JSONFunctions.ts +61 -1
  22. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +60 -21
  23. package/UI/Components/Dictionary/Dictionary.tsx +188 -26
  24. package/UI/Components/Dictionary/DictionaryFilterOperator.ts +357 -0
  25. package/UI/Components/Dictionary/DictionaryOfStrings.tsx +12 -7
  26. package/UI/Components/EditionLabel/EditionLabel.tsx +224 -10
  27. package/UI/Components/Filters/FilterViewer.tsx +81 -16
  28. package/UI/Components/Filters/FiltersForm.tsx +18 -3
  29. package/UI/Components/Filters/JSONFilter.tsx +11 -2
  30. package/UI/Components/Filters/Types/Filter.ts +3 -0
  31. package/UI/Components/Forms/Fields/FormField.tsx +6 -1
  32. package/UI/Components/Forms/Types/Field.ts +5 -0
  33. package/UI/Components/LogsViewer/LogsViewer.tsx +73 -4
  34. package/UI/Components/LogsViewer/components/LogSearchBar.tsx +77 -31
  35. package/UI/Components/LogsViewer/components/LogSearchSuggestions.tsx +44 -1
  36. package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +7 -5
  37. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +6 -0
  38. package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +84 -25
  39. package/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.tsx +44 -1
  40. package/Utils/Dashboard/Components/DashboardAlertListComponent.ts +86 -0
  41. package/Utils/Dashboard/Components/DashboardIncidentListComponent.ts +86 -0
  42. package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +85 -0
  43. package/Utils/Dashboard/Components/Index.ts +21 -0
  44. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +57 -0
  45. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  46. package/build/dist/Models/DatabaseModels/GlobalConfig.js +54 -0
  47. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  48. package/build/dist/Server/API/EnterpriseLicenseAPI.js +64 -1
  49. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  50. package/build/dist/Server/API/GlobalConfigAPI.js +47 -0
  51. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  52. package/build/dist/Server/API/MetricAPI.js +123 -0
  53. package/build/dist/Server/API/MetricAPI.js.map +1 -0
  54. package/build/dist/Server/API/TelemetryAPI.js +9 -0
  55. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  56. package/build/dist/Server/EnvironmentConfig.js +3 -0
  57. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  58. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.js +26 -0
  59. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.js.map +1 -0
  60. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  61. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  62. package/build/dist/Server/Infrastructure/Queue.js +3 -3
  63. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  64. package/build/dist/Server/Services/AnalyticsDatabaseService.js +18 -0
  65. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  66. package/build/dist/Server/Services/MetricService.js +151 -1
  67. package/build/dist/Server/Services/MetricService.js.map +1 -1
  68. package/build/dist/Server/Services/TelemetryAttributeService.js +36 -7
  69. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  70. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +135 -5
  71. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  72. package/build/dist/Tests/Types/Date.test.js +40 -0
  73. package/build/dist/Tests/Types/Date.test.js.map +1 -1
  74. package/build/dist/Types/Dashboard/DashboardComponentType.js +3 -0
  75. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  76. package/build/dist/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.js +2 -0
  77. package/build/dist/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.js.map +1 -0
  78. package/build/dist/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.js +2 -0
  79. package/build/dist/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.js.map +1 -0
  80. package/build/dist/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.js +2 -0
  81. package/build/dist/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.js.map +1 -0
  82. package/build/dist/Types/Date.js +7 -2
  83. package/build/dist/Types/Date.js.map +1 -1
  84. package/build/dist/Types/JSONFunctions.js +47 -1
  85. package/build/dist/Types/JSONFunctions.js.map +1 -1
  86. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +21 -10
  87. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  88. package/build/dist/UI/Components/Dictionary/Dictionary.js +109 -16
  89. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  90. package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js +263 -0
  91. package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js.map +1 -0
  92. package/build/dist/UI/Components/Dictionary/DictionaryOfStrings.js +10 -6
  93. package/build/dist/UI/Components/Dictionary/DictionaryOfStrings.js.map +1 -1
  94. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +124 -6
  95. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  96. package/build/dist/UI/Components/Filters/FilterViewer.js +50 -12
  97. package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
  98. package/build/dist/UI/Components/Filters/FiltersForm.js +5 -4
  99. package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
  100. package/build/dist/UI/Components/Filters/JSONFilter.js +1 -1
  101. package/build/dist/UI/Components/Filters/JSONFilter.js.map +1 -1
  102. package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
  103. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  104. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +54 -5
  105. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  106. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +59 -29
  107. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -1
  108. package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js +10 -2
  109. package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js.map +1 -1
  110. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +2 -5
  111. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -1
  112. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
  113. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  114. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +59 -22
  115. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -1
  116. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js +10 -2
  117. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js.map +1 -1
  118. package/build/dist/Utils/Dashboard/Components/DashboardAlertListComponent.js +70 -0
  119. package/build/dist/Utils/Dashboard/Components/DashboardAlertListComponent.js.map +1 -0
  120. package/build/dist/Utils/Dashboard/Components/DashboardIncidentListComponent.js +70 -0
  121. package/build/dist/Utils/Dashboard/Components/DashboardIncidentListComponent.js.map +1 -0
  122. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +69 -0
  123. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -0
  124. package/build/dist/Utils/Dashboard/Components/Index.js +12 -0
  125. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  126. package/package.json +1 -1
@@ -101,4 +101,58 @@ export default class EnterpriseLicense extends BaseModel {
101
101
  type: ColumnType.Number,
102
102
  })
103
103
  public annualContractValue?: number = undefined;
104
+
105
+ @ColumnAccessControl({
106
+ create: [],
107
+ read: [],
108
+ update: [],
109
+ })
110
+ @TableColumn({
111
+ required: false,
112
+ type: TableColumnType.Number,
113
+ title: "User Limit",
114
+ description:
115
+ "Maximum number of users allowed under this enterprise license.",
116
+ })
117
+ @Column({
118
+ nullable: true,
119
+ type: ColumnType.Number,
120
+ })
121
+ public userLimit?: number = undefined;
122
+
123
+ @ColumnAccessControl({
124
+ create: [],
125
+ read: [],
126
+ update: [],
127
+ })
128
+ @TableColumn({
129
+ required: false,
130
+ type: TableColumnType.Number,
131
+ title: "Current User Count",
132
+ description:
133
+ "Most recent user count reported by the customer's self-hosted installation.",
134
+ })
135
+ @Column({
136
+ nullable: true,
137
+ type: ColumnType.Number,
138
+ })
139
+ public currentUserCount?: number = undefined;
140
+
141
+ @ColumnAccessControl({
142
+ create: [],
143
+ read: [],
144
+ update: [],
145
+ })
146
+ @TableColumn({
147
+ required: false,
148
+ type: TableColumnType.Date,
149
+ title: "User Count Updated At",
150
+ description:
151
+ "Timestamp of the most recent user count report from the customer's self-hosted installation.",
152
+ })
153
+ @Column({
154
+ nullable: true,
155
+ type: ColumnType.Date,
156
+ })
157
+ public userCountUpdatedAt?: Date = undefined;
104
158
  }
@@ -756,6 +756,57 @@ export default class GlobalConfig extends GlobalConfigModel {
756
756
  })
757
757
  public enterpriseLicenseToken?: string = undefined;
758
758
 
759
+ @ColumnAccessControl({
760
+ create: [],
761
+ read: [],
762
+ update: [],
763
+ })
764
+ @TableColumn({
765
+ type: TableColumnType.Number,
766
+ title: "Enterprise License User Limit",
767
+ description:
768
+ "Maximum number of users permitted under the validated enterprise license.",
769
+ })
770
+ @Column({
771
+ type: ColumnType.Number,
772
+ nullable: true,
773
+ })
774
+ public enterpriseLicenseUserLimit?: number = undefined;
775
+
776
+ @ColumnAccessControl({
777
+ create: [],
778
+ read: [],
779
+ update: [],
780
+ })
781
+ @TableColumn({
782
+ type: TableColumnType.Number,
783
+ title: "Enterprise License Current User Count",
784
+ description:
785
+ "User count last reported to OneUptime for the validated enterprise license.",
786
+ })
787
+ @Column({
788
+ type: ColumnType.Number,
789
+ nullable: true,
790
+ })
791
+ public enterpriseLicenseCurrentUserCount?: number = undefined;
792
+
793
+ @ColumnAccessControl({
794
+ create: [],
795
+ read: [],
796
+ update: [],
797
+ })
798
+ @TableColumn({
799
+ type: TableColumnType.Date,
800
+ title: "Enterprise License User Count Updated At",
801
+ description:
802
+ "Timestamp of the most recent user count report sent to OneUptime for the validated enterprise license.",
803
+ })
804
+ @Column({
805
+ type: ColumnType.Date,
806
+ nullable: true,
807
+ })
808
+ public enterpriseLicenseUserCountUpdatedAt?: Date = undefined;
809
+
759
810
  @ColumnAccessControl({
760
811
  create: [],
761
812
  read: [],
@@ -6,6 +6,7 @@ import EnterpriseLicenseService, {
6
6
  } from "../Services/EnterpriseLicenseService";
7
7
  import UserMiddleware from "../Middleware/UserAuthorization";
8
8
  import JSONWebToken from "../Utils/JsonWebToken";
9
+ import OneUptimeDate from "../../Types/Date";
9
10
  import Response from "../Utils/Response";
10
11
  import {
11
12
  ExpressRequest,
@@ -52,6 +53,9 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
52
53
  companyName: true,
53
54
  expiresAt: true,
54
55
  licenseKey: true,
56
+ userLimit: true,
57
+ currentUserCount: true,
58
+ userCountUpdatedAt: true,
55
59
  },
56
60
  props: {
57
61
  isRoot: true,
@@ -80,6 +84,8 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
80
84
  companyName: license.companyName || "",
81
85
  expiresAt: license.expiresAt.toISOString(),
82
86
  licenseKey: license.licenseKey || "",
87
+ userLimit:
88
+ typeof license.userLimit === "number" ? license.userLimit : null,
83
89
  };
84
90
 
85
91
  const token: string = JSONWebToken.signJsonPayload(
@@ -91,6 +97,14 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
91
97
  companyName: payload["companyName"] as string,
92
98
  expiresAt: payload["expiresAt"] as string,
93
99
  licenseKey: payload["licenseKey"] as string,
100
+ userLimit: payload["userLimit"],
101
+ currentUserCount:
102
+ typeof license.currentUserCount === "number"
103
+ ? license.currentUserCount
104
+ : null,
105
+ userCountUpdatedAt: license.userCountUpdatedAt
106
+ ? license.userCountUpdatedAt.toISOString()
107
+ : null,
94
108
  token,
95
109
  });
96
110
  } catch (err) {
@@ -98,5 +112,74 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
98
112
  }
99
113
  },
100
114
  );
115
+
116
+ this.router.post(
117
+ `${new this.entityType().getCrudApiPath()?.toString()}/report-user-count`,
118
+ async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
119
+ try {
120
+ const licenseKey: string | undefined = (
121
+ req.body["licenseKey"] as string | undefined
122
+ )?.trim();
123
+ const rawUserCount: unknown = req.body["userCount"];
124
+
125
+ if (!licenseKey) {
126
+ throw new BadDataException("License key is required");
127
+ }
128
+
129
+ const userCount: number = Number(rawUserCount);
130
+
131
+ if (
132
+ !Number.isFinite(userCount) ||
133
+ userCount < 0 ||
134
+ !Number.isInteger(userCount)
135
+ ) {
136
+ throw new BadDataException(
137
+ "userCount must be a non-negative integer",
138
+ );
139
+ }
140
+
141
+ const license: EnterpriseLicense | null =
142
+ await EnterpriseLicenseService.findOneBy({
143
+ query: {
144
+ licenseKey: licenseKey,
145
+ },
146
+ select: {
147
+ _id: true,
148
+ userLimit: true,
149
+ },
150
+ props: {
151
+ isRoot: true,
152
+ },
153
+ });
154
+
155
+ if (!license) {
156
+ throw new BadDataException("License key is invalid");
157
+ }
158
+
159
+ const reportedAt: Date = OneUptimeDate.getCurrentDate();
160
+
161
+ await EnterpriseLicenseService.updateOneById({
162
+ id: license.id!,
163
+ data: {
164
+ currentUserCount: userCount,
165
+ userCountUpdatedAt: reportedAt,
166
+ },
167
+ props: {
168
+ isRoot: true,
169
+ ignoreHooks: true,
170
+ },
171
+ });
172
+
173
+ return Response.sendJsonObjectResponse(req, res, {
174
+ currentUserCount: userCount,
175
+ userCountUpdatedAt: reportedAt.toISOString(),
176
+ userLimit:
177
+ typeof license.userLimit === "number" ? license.userLimit : null,
178
+ });
179
+ } catch (err) {
180
+ next(err);
181
+ }
182
+ },
183
+ );
101
184
  }
102
185
  }
@@ -67,6 +67,9 @@ export default class GlobalConfigAPI extends BaseAPI<
67
67
  enterpriseLicenseExpiresAt: true,
68
68
  enterpriseLicenseKey: true,
69
69
  enterpriseLicenseToken: true,
70
+ enterpriseLicenseUserLimit: true,
71
+ enterpriseLicenseCurrentUserCount: true,
72
+ enterpriseLicenseUserCountUpdatedAt: true,
70
73
  },
71
74
  props: {
72
75
  isRoot: true,
@@ -80,6 +83,17 @@ export default class GlobalConfigAPI extends BaseAPI<
80
83
  : null,
81
84
  licenseKey: config?.enterpriseLicenseKey || null,
82
85
  token: config?.enterpriseLicenseToken || null,
86
+ userLimit:
87
+ typeof config?.enterpriseLicenseUserLimit === "number"
88
+ ? config.enterpriseLicenseUserLimit
89
+ : null,
90
+ currentUserCount:
91
+ typeof config?.enterpriseLicenseCurrentUserCount === "number"
92
+ ? config.enterpriseLicenseCurrentUserCount
93
+ : null,
94
+ userCountUpdatedAt: config?.enterpriseLicenseUserCountUpdatedAt
95
+ ? config.enterpriseLicenseUserCountUpdatedAt.toISOString()
96
+ : null,
83
97
  };
84
98
 
85
99
  return Response.sendJsonObjectResponse(req, res, responseBody);
@@ -143,11 +157,38 @@ export default class GlobalConfigAPI extends BaseAPI<
143
157
  licenseExpiry = parsedDate;
144
158
  }
145
159
 
160
+ const userLimitRaw: unknown = payload["userLimit"];
161
+ const userLimit: number | null =
162
+ typeof userLimitRaw === "number" && Number.isFinite(userLimitRaw)
163
+ ? userLimitRaw
164
+ : null;
165
+
166
+ const currentUserCountRaw: unknown = payload["currentUserCount"];
167
+ const currentUserCount: number | null =
168
+ typeof currentUserCountRaw === "number" &&
169
+ Number.isFinite(currentUserCountRaw)
170
+ ? currentUserCountRaw
171
+ : null;
172
+
173
+ const userCountUpdatedAtRaw: string | undefined = payload[
174
+ "userCountUpdatedAt"
175
+ ] as string | undefined;
176
+ let userCountUpdatedAt: Date | null = null;
177
+ if (userCountUpdatedAtRaw) {
178
+ const parsedReportedAt: Date = new Date(userCountUpdatedAtRaw);
179
+ if (!Number.isNaN(parsedReportedAt.getTime())) {
180
+ userCountUpdatedAt = parsedReportedAt;
181
+ }
182
+ }
183
+
146
184
  const updatePayload: PartialEntity<GlobalConfig> = {
147
185
  enterpriseCompanyName: companyNameRaw || null,
148
186
  enterpriseLicenseKey: licenseKeyRaw || null,
149
187
  enterpriseLicenseExpiresAt: licenseExpiry || null,
150
188
  enterpriseLicenseToken: licenseToken || null,
189
+ enterpriseLicenseUserLimit: userLimit,
190
+ enterpriseLicenseCurrentUserCount: currentUserCount,
191
+ enterpriseLicenseUserCountUpdatedAt: userCountUpdatedAt,
151
192
  };
152
193
 
153
194
  const globalConfigId: ObjectID = ObjectID.getZeroObjectID();
@@ -193,6 +234,19 @@ export default class GlobalConfigAPI extends BaseAPI<
193
234
  newConfig.enterpriseLicenseExpiresAt = licenseExpiry;
194
235
  }
195
236
 
237
+ if (userLimit !== null) {
238
+ newConfig.enterpriseLicenseUserLimit = userLimit;
239
+ }
240
+
241
+ if (currentUserCount !== null) {
242
+ newConfig.enterpriseLicenseCurrentUserCount = currentUserCount;
243
+ }
244
+
245
+ if (userCountUpdatedAt) {
246
+ newConfig.enterpriseLicenseUserCountUpdatedAt =
247
+ userCountUpdatedAt;
248
+ }
249
+
196
250
  await GlobalConfigService.create({
197
251
  data: newConfig,
198
252
  props: {
@@ -207,6 +261,11 @@ export default class GlobalConfigAPI extends BaseAPI<
207
261
  expiresAt: licenseExpiry ? licenseExpiry.toISOString() : null,
208
262
  licenseKey: licenseKeyRaw || null,
209
263
  token: licenseToken || null,
264
+ userLimit: userLimit,
265
+ currentUserCount: currentUserCount,
266
+ userCountUpdatedAt: userCountUpdatedAt
267
+ ? userCountUpdatedAt.toISOString()
268
+ : null,
210
269
  });
211
270
  } catch (err) {
212
271
  next(err);
@@ -0,0 +1,149 @@
1
+ import AggregateBy from "../../Types/BaseDatabase/AggregateBy";
2
+ import AggregatedResult from "../../Types/BaseDatabase/AggregatedResult";
3
+ import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
4
+ import BadRequestException from "../../Types/Exception/BadRequestException";
5
+ import { JSONObject } from "../../Types/JSON";
6
+ import JSONFunctions from "../../Types/JSONFunctions";
7
+ import Metric from "../../Models/AnalyticsModels/Metric";
8
+ import { MetricService } from "../Services/MetricService";
9
+ import GlobalCache from "../Infrastructure/GlobalCache";
10
+ import logger from "../Utils/Logger";
11
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
12
+ import { ExpressRequest, ExpressResponse } from "../Utils/Express";
13
+ import Response from "../Utils/Response";
14
+ import CommonAPI from "./CommonAPI";
15
+ import BaseAnalyticsAPI from "./BaseAnalyticsAPI";
16
+
17
+ /*
18
+ * Aggregate cache TTL. Dashboards typically auto-refresh every 30s+, so
19
+ * an 8s window collapses bursts of identical requests (e.g. 12 widgets
20
+ * loading on the same page) onto a single ClickHouse query while still
21
+ * looking real-time to humans.
22
+ */
23
+ const AGGREGATE_CACHE_NAMESPACE: string = "metric-aggregate";
24
+ const AGGREGATE_CACHE_TTL_SECONDS: number = 8;
25
+
26
+ export default class MetricAPI extends BaseAnalyticsAPI<Metric, MetricService> {
27
+ public constructor(service: MetricService) {
28
+ super(Metric, service);
29
+ }
30
+
31
+ /*
32
+ * Cached override of BaseAnalyticsAPI.getAggregate.
33
+ *
34
+ * Why a cache: each chart/value/gauge/table widget on a dashboard
35
+ * issues its own /aggregate call. With 10+ widgets and a small group
36
+ * of users hitting the same dashboard the underlying ClickHouse
37
+ * cluster sees the same heavy aggregation many times in close
38
+ * succession. Aggregations are read-only and pure (same input ->
39
+ * same output for the bucket interval), so a brief result cache is
40
+ * safe.
41
+ *
42
+ * Cache key: tenant project + the deserialized aggregateBy payload.
43
+ * We must include the project so cross-tenant collisions cannot
44
+ * leak data; we deliberately do NOT key on user id, because the
45
+ * service layer applies project-scoped read permissions and metric
46
+ * data is project-wide.
47
+ *
48
+ * Cache miss / Redis down: we fall through to the live query, so
49
+ * cache outages degrade to today's behavior, never error.
50
+ */
51
+ @CaptureSpan()
52
+ public override async getAggregate(
53
+ req: ExpressRequest,
54
+ res: ExpressResponse,
55
+ ): Promise<void> {
56
+ await this.onBeforeList(req, res);
57
+
58
+ let aggregateBy: AggregateBy<Metric> | null = null;
59
+
60
+ if (req.body && req.body["aggregateBy"]) {
61
+ aggregateBy = JSONFunctions.deserialize(
62
+ req.body["aggregateBy"] as JSONObject,
63
+ ) as any;
64
+ }
65
+
66
+ if (!aggregateBy) {
67
+ throw new BadRequestException("AggregateBy is required");
68
+ }
69
+
70
+ const databaseProps: DatabaseCommonInteractionProps =
71
+ await CommonAPI.getDatabaseCommonInteractionProps(req);
72
+
73
+ const projectId: string | undefined = databaseProps.tenantId?.toString();
74
+ const cacheKey: string | null = projectId
75
+ ? `${projectId}:${this.buildCacheKey(aggregateBy)}`
76
+ : null;
77
+
78
+ if (cacheKey) {
79
+ try {
80
+ const cached: JSONObject | null = await GlobalCache.getJSONObject(
81
+ AGGREGATE_CACHE_NAMESPACE,
82
+ cacheKey,
83
+ );
84
+ if (cached) {
85
+ return Response.sendJsonObjectResponse(req, res, cached);
86
+ }
87
+ } catch (err) {
88
+ // Cache fetch failed — fall through to a live query.
89
+ logger.debug("MetricAPI aggregate cache read failed");
90
+ logger.debug(err);
91
+ }
92
+ }
93
+
94
+ const aggregateResult: AggregatedResult = await this.service.aggregateBy({
95
+ ...aggregateBy,
96
+ props: databaseProps,
97
+ });
98
+
99
+ const responseBody: JSONObject = { ...(aggregateResult as any) };
100
+
101
+ if (cacheKey) {
102
+ try {
103
+ await GlobalCache.setJSON(
104
+ AGGREGATE_CACHE_NAMESPACE,
105
+ cacheKey,
106
+ responseBody,
107
+ { expiresInSeconds: AGGREGATE_CACHE_TTL_SECONDS },
108
+ );
109
+ } catch (err) {
110
+ logger.debug("MetricAPI aggregate cache write failed");
111
+ logger.debug(err);
112
+ }
113
+ }
114
+
115
+ return Response.sendJsonObjectResponse(req, res, responseBody);
116
+ }
117
+
118
+ private buildCacheKey(aggregateBy: AggregateBy<Metric>): string {
119
+ /*
120
+ * Stable serialization. Date instances are normalized to ISO so two
121
+ * logically-equal time windows hit the same cache slot, and we sort
122
+ * keys via JSON.stringify replacer to keep ordering deterministic
123
+ * across clients and across versions of V8.
124
+ */
125
+ return JSON.stringify(
126
+ aggregateBy,
127
+ (_key: string, value: unknown): unknown => {
128
+ if (value instanceof Date) {
129
+ return value.toISOString();
130
+ }
131
+ if (
132
+ value &&
133
+ typeof value === "object" &&
134
+ !Array.isArray(value) &&
135
+ (value as Record<string, unknown>).constructor === Object
136
+ ) {
137
+ const sorted: Record<string, unknown> = {};
138
+ for (const k of Object.keys(
139
+ value as Record<string, unknown>,
140
+ ).sort()) {
141
+ sorted[k] = (value as Record<string, unknown>)[k];
142
+ }
143
+ return sorted;
144
+ }
145
+ return value;
146
+ },
147
+ );
148
+ }
149
+ }
@@ -80,6 +80,14 @@ router.post(
80
80
  },
81
81
  );
82
82
 
83
+ router.post(
84
+ "/telemetry/logs/get-attribute-values",
85
+ UserMiddleware.getUserMiddleware,
86
+ async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
87
+ return getAttributeValues(req, res, next, TelemetryType.Log);
88
+ },
89
+ );
90
+
83
91
  router.post(
84
92
  "/telemetry/traces/get-attributes",
85
93
  UserMiddleware.getUserMiddleware,
@@ -96,6 +104,22 @@ router.post(
96
104
  },
97
105
  );
98
106
 
107
+ router.post(
108
+ "/telemetry/exceptions/get-attributes",
109
+ UserMiddleware.getUserMiddleware,
110
+ async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
111
+ return getAttributes(req, res, next, TelemetryType.Exception);
112
+ },
113
+ );
114
+
115
+ router.post(
116
+ "/telemetry/exceptions/get-attribute-values",
117
+ UserMiddleware.getUserMiddleware,
118
+ async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
119
+ return getAttributeValues(req, res, next, TelemetryType.Exception);
120
+ },
121
+ );
122
+
99
123
  type GetAttributesFunction = (
100
124
  req: ExpressRequest,
101
125
  res: ExpressResponse,
@@ -162,6 +162,12 @@ export const ClusterKey: ObjectID = new ObjectID(
162
162
 
163
163
  export const HasClusterKey: boolean = Boolean(process.env["ONEUPTIME_SECRET"]);
164
164
 
165
+ export const EnableQueueDashboard: boolean =
166
+ process.env["ENABLE_QUEUE_DASHBOARD"] === "true";
167
+
168
+ export const QueueDashboardSecret: string =
169
+ process.env["QUEUE_DASHBOARD_SECRET"] || "";
170
+
165
171
  export const RegisterProbeKey: ObjectID = new ObjectID(
166
172
  process.env["REGISTER_PROBE_KEY"] || "secret",
167
173
  );
@@ -513,6 +519,10 @@ export const EnterpriseLicenseValidationUrl: URL = URL.fromString(
513
519
  "https://oneuptime.com/api/enterprise-license/validate",
514
520
  );
515
521
 
522
+ export const EnterpriseLicenseUserCountReportUrl: URL = URL.fromString(
523
+ "https://oneuptime.com/api/enterprise-license/report-user-count",
524
+ );
525
+
516
526
  // Inbound Email Configuration for Incoming Email Monitor
517
527
  export enum InboundEmailProviderType {
518
528
  SendGrid = "SendGrid",
@@ -0,0 +1,59 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class MigrationName1777629313843 implements MigrationInterface {
4
+ public name: string = "MigrationName1777629313843";
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(
8
+ `ALTER TABLE "GlobalConfig" ADD "enterpriseLicenseUserLimit" integer`,
9
+ );
10
+ await queryRunner.query(
11
+ `ALTER TABLE "GlobalConfig" ADD "enterpriseLicenseCurrentUserCount" integer`,
12
+ );
13
+ await queryRunner.query(
14
+ `ALTER TABLE "GlobalConfig" ADD "enterpriseLicenseUserCountUpdatedAt" TIMESTAMP WITH TIME ZONE`,
15
+ );
16
+ await queryRunner.query(
17
+ `ALTER TABLE "EnterpriseLicense" ADD "userLimit" integer`,
18
+ );
19
+ await queryRunner.query(
20
+ `ALTER TABLE "EnterpriseLicense" ADD "currentUserCount" integer`,
21
+ );
22
+ await queryRunner.query(
23
+ `ALTER TABLE "EnterpriseLicense" ADD "userCountUpdatedAt" TIMESTAMP WITH TIME ZONE`,
24
+ );
25
+ await queryRunner.query(
26
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
27
+ );
28
+ await queryRunner.query(
29
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
30
+ );
31
+ }
32
+
33
+ public async down(queryRunner: QueryRunner): Promise<void> {
34
+ await queryRunner.query(
35
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
36
+ );
37
+ await queryRunner.query(
38
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
39
+ );
40
+ await queryRunner.query(
41
+ `ALTER TABLE "EnterpriseLicense" DROP COLUMN "userCountUpdatedAt"`,
42
+ );
43
+ await queryRunner.query(
44
+ `ALTER TABLE "EnterpriseLicense" DROP COLUMN "currentUserCount"`,
45
+ );
46
+ await queryRunner.query(
47
+ `ALTER TABLE "EnterpriseLicense" DROP COLUMN "userLimit"`,
48
+ );
49
+ await queryRunner.query(
50
+ `ALTER TABLE "GlobalConfig" DROP COLUMN "enterpriseLicenseUserCountUpdatedAt"`,
51
+ );
52
+ await queryRunner.query(
53
+ `ALTER TABLE "GlobalConfig" DROP COLUMN "enterpriseLicenseCurrentUserCount"`,
54
+ );
55
+ await queryRunner.query(
56
+ `ALTER TABLE "GlobalConfig" DROP COLUMN "enterpriseLicenseUserLimit"`,
57
+ );
58
+ }
59
+ }
@@ -295,6 +295,7 @@ import { AddTelemetryRetentionSettings1777018175127 } from "./1777018175127-AddT
295
295
  import { AddMonitorTemplate1777201966799 } from "./1777201966799-AddMonitorTemplate";
296
296
  import { MigrationName1777550162848 } from "./1777550162848-MigrationName";
297
297
  import { MigrationName1777571961028 } from "./1777571961028-MigrationName";
298
+ import { MigrationName1777629313843 } from "./1777629313843-MigrationName";
298
299
  export default [
299
300
  InitialMigration,
300
301
  MigrationName1717678334852,
@@ -593,4 +594,5 @@ export default [
593
594
  AddMonitorTemplate1777201966799,
594
595
  MigrationName1777550162848,
595
596
  MigrationName1777571961028,
597
+ MigrationName1777629313843,
596
598
  ];
@@ -1,4 +1,4 @@
1
- import { ClusterKey } from "../EnvironmentConfig";
1
+ import { QueueDashboardSecret } from "../EnvironmentConfig";
2
2
  import Dictionary from "../../Types/Dictionary";
3
3
  import { JSONObject } from "../../Types/JSON";
4
4
  import { Queue as BullQueue, Job, JobsOptions, RepeatableJob } from "bullmq";
@@ -153,7 +153,7 @@ export default class Queue {
153
153
 
154
154
  @CaptureSpan()
155
155
  public static getInspectorRoute(): string {
156
- return "/worker/inspect/queue/:clusterKey";
156
+ return "/worker/inspect/queue/:dashboardSecret";
157
157
  }
158
158
 
159
159
  @CaptureSpan()
@@ -174,8 +174,8 @@ export default class Queue {
174
174
 
175
175
  serverAdapter.setBasePath(
176
176
  this.getInspectorRoute().replace(
177
- "/:clusterKey",
178
- "/" + ClusterKey.toString(),
177
+ "/:dashboardSecret",
178
+ "/" + QueueDashboardSecret,
179
179
  ),
180
180
  );
181
181
 
@@ -787,6 +787,27 @@ export default class AnalyticsDatabaseService<
787
787
  }}
788
788
  `);
789
789
 
790
+ /*
791
+ * Aggregation read-path settings.
792
+ *
793
+ * - optimize_aggregation_in_order: when GROUP BY is a prefix of the
794
+ * sort key (we always group by a time bucket and the time column
795
+ * is at the tail of every analytics primary key), ClickHouse can
796
+ * stream rows in order and emit aggregates without an in-memory
797
+ * sort, which is a large speedup on wide time ranges.
798
+ * - optimize_move_to_prewhere: PREWHERE is a default-on optimizer
799
+ * pass; we set it explicitly so the behavior is independent of
800
+ * server-side defaults.
801
+ * - max_threads=4: caps per-query parallelism so a single dashboard
802
+ * load (which fans out to many aggregate calls) does not starve
803
+ * other tenants on the cluster. Per-query latency is essentially
804
+ * unchanged at 4 threads for the usual dashboard widget time
805
+ * ranges, but cluster headroom is preserved under burst.
806
+ */
807
+ statement.append(
808
+ ` SETTINGS optimize_aggregation_in_order=1, optimize_move_to_prewhere=1, max_threads=4`,
809
+ );
810
+
790
811
  logger.debug(`${this.model.tableName} Aggregate Statement`, { tableName: this.model.tableName } as LogAttributes);
791
812
  logger.debug(statement, { tableName: this.model.tableName } as LogAttributes);
792
813