@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.
- package/Models/DatabaseModels/EnterpriseLicense.ts +54 -0
- package/Models/DatabaseModels/GlobalConfig.ts +51 -0
- package/Server/API/EnterpriseLicenseAPI.ts +83 -0
- package/Server/API/GlobalConfigAPI.ts +59 -0
- package/Server/API/MetricAPI.ts +149 -0
- package/Server/API/TelemetryAPI.ts +24 -0
- package/Server/EnvironmentConfig.ts +10 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.ts +59 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Infrastructure/Queue.ts +4 -4
- package/Server/Services/AnalyticsDatabaseService.ts +21 -0
- package/Server/Services/MetricService.ts +193 -1
- package/Server/Services/TelemetryAttributeService.ts +37 -3
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +174 -7
- package/Tests/Types/Date.test.ts +46 -0
- package/Types/Dashboard/DashboardComponentType.ts +3 -0
- package/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.ts +13 -0
- package/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.ts +13 -0
- package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +13 -0
- package/Types/Date.ts +9 -4
- package/Types/JSONFunctions.ts +61 -1
- package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +60 -21
- package/UI/Components/Dictionary/Dictionary.tsx +188 -26
- package/UI/Components/Dictionary/DictionaryFilterOperator.ts +357 -0
- package/UI/Components/Dictionary/DictionaryOfStrings.tsx +12 -7
- package/UI/Components/EditionLabel/EditionLabel.tsx +224 -10
- package/UI/Components/Filters/FilterViewer.tsx +81 -16
- package/UI/Components/Filters/FiltersForm.tsx +18 -3
- package/UI/Components/Filters/JSONFilter.tsx +11 -2
- package/UI/Components/Filters/Types/Filter.ts +3 -0
- package/UI/Components/Forms/Fields/FormField.tsx +6 -1
- package/UI/Components/Forms/Types/Field.ts +5 -0
- package/UI/Components/LogsViewer/LogsViewer.tsx +73 -4
- package/UI/Components/LogsViewer/components/LogSearchBar.tsx +77 -31
- package/UI/Components/LogsViewer/components/LogSearchSuggestions.tsx +44 -1
- package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +7 -5
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +6 -0
- package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +84 -25
- package/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.tsx +44 -1
- package/Utils/Dashboard/Components/DashboardAlertListComponent.ts +86 -0
- package/Utils/Dashboard/Components/DashboardIncidentListComponent.ts +86 -0
- package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +85 -0
- package/Utils/Dashboard/Components/Index.ts +21 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +57 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +54 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +64 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/API/GlobalConfigAPI.js +47 -0
- package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
- package/build/dist/Server/API/MetricAPI.js +123 -0
- package/build/dist/Server/API/MetricAPI.js.map +1 -0
- package/build/dist/Server/API/TelemetryAPI.js +9 -0
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +3 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Queue.js +3 -3
- package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +18 -0
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/MetricService.js +151 -1
- package/build/dist/Server/Services/MetricService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryAttributeService.js +36 -7
- package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +135 -5
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Tests/Types/Date.test.js +40 -0
- package/build/dist/Tests/Types/Date.test.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponentType.js +3 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.js.map +1 -0
- package/build/dist/Types/Date.js +7 -2
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/JSONFunctions.js +47 -1
- package/build/dist/Types/JSONFunctions.js.map +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +21 -10
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/Dictionary.js +109 -16
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js +263 -0
- package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js.map +1 -0
- package/build/dist/UI/Components/Dictionary/DictionaryOfStrings.js +10 -6
- package/build/dist/UI/Components/Dictionary/DictionaryOfStrings.js.map +1 -1
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +124 -6
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/build/dist/UI/Components/Filters/FilterViewer.js +50 -12
- package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
- package/build/dist/UI/Components/Filters/FiltersForm.js +5 -4
- 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/Forms/Fields/FormField.js +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +54 -5
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +59 -29
- package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js +10 -2
- package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +2 -5
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +59 -22
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js +10 -2
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js.map +1 -1
- package/build/dist/Utils/Dashboard/Components/DashboardAlertListComponent.js +70 -0
- package/build/dist/Utils/Dashboard/Components/DashboardAlertListComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardIncidentListComponent.js +70 -0
- package/build/dist/Utils/Dashboard/Components/DashboardIncidentListComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +69 -0
- package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/Index.js +12 -0
- package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
- 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 {
|
|
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/:
|
|
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
|
-
"/:
|
|
178
|
-
"/" +
|
|
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
|
|