@oneuptime/common 11.5.5 → 11.5.6
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/User.ts +47 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784048917994-AddCreatedByUserToUser.ts +23 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Middleware/MasterAdminAuthorization.ts +37 -0
- package/Server/Middleware/ProjectAuthorization.ts +40 -15
- package/Server/Services/TeamMemberService.ts +2 -0
- package/Server/Services/UserService.ts +10 -0
- package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +16 -6
- package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +14 -0
- package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +14 -3
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +130 -12
- package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilderUnits.test.ts +553 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageBuilderUnits.test.ts +590 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageFormatterUnits.test.ts +254 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +821 -0
- package/build/dist/Models/DatabaseModels/User.js +46 -0
- package/build/dist/Models/DatabaseModels/User.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784048917994-AddCreatedByUserToUser.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784048917994-AddCreatedByUserToUser.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/Middleware/MasterAdminAuthorization.js +26 -0
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js +40 -14
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +2 -0
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +8 -0
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +15 -7
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +12 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +8 -3
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +96 -12
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/package.json +1 -1
|
@@ -647,6 +647,53 @@ class User extends UserModel {
|
|
|
647
647
|
})
|
|
648
648
|
public tempAlertPhoneNumber?: Phone = undefined;
|
|
649
649
|
|
|
650
|
+
@ColumnAccessControl({
|
|
651
|
+
create: [],
|
|
652
|
+
read: [],
|
|
653
|
+
update: [],
|
|
654
|
+
})
|
|
655
|
+
@TableColumn({
|
|
656
|
+
manyToOneRelationColumn: "createdByUserId",
|
|
657
|
+
type: TableColumnType.Entity,
|
|
658
|
+
title: "Created by User",
|
|
659
|
+
modelType: User,
|
|
660
|
+
description:
|
|
661
|
+
"Relation to the User who created (invited) this user, if this user was invited to OneUptime by another user.",
|
|
662
|
+
})
|
|
663
|
+
@ManyToOne(
|
|
664
|
+
() => {
|
|
665
|
+
return User;
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
cascade: false,
|
|
669
|
+
eager: false,
|
|
670
|
+
nullable: true,
|
|
671
|
+
onDelete: "SET NULL",
|
|
672
|
+
orphanedRowAction: "nullify",
|
|
673
|
+
},
|
|
674
|
+
)
|
|
675
|
+
@JoinColumn({ name: "createdByUserId" })
|
|
676
|
+
public createdByUser?: User = undefined;
|
|
677
|
+
|
|
678
|
+
@ColumnAccessControl({
|
|
679
|
+
create: [],
|
|
680
|
+
read: [],
|
|
681
|
+
update: [],
|
|
682
|
+
})
|
|
683
|
+
@TableColumn({
|
|
684
|
+
type: TableColumnType.ObjectID,
|
|
685
|
+
title: "Created by User ID",
|
|
686
|
+
description:
|
|
687
|
+
"User ID who created (invited) this user, if this user was invited to OneUptime by another user.",
|
|
688
|
+
example: "b2c3d4e5-f6a7-8901-bcde-f12345678901",
|
|
689
|
+
})
|
|
690
|
+
@Column({
|
|
691
|
+
type: ColumnType.ObjectID,
|
|
692
|
+
nullable: true,
|
|
693
|
+
transformer: ObjectID.getDatabaseTransformer(),
|
|
694
|
+
})
|
|
695
|
+
public createdByUserId?: ObjectID = undefined;
|
|
696
|
+
|
|
650
697
|
@ColumnAccessControl({
|
|
651
698
|
create: [],
|
|
652
699
|
read: [],
|
package/Server/Infrastructure/Postgres/SchemaMigrations/1784048917994-AddCreatedByUserToUser.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class AddCreatedByUserToUser1784048917994 implements MigrationInterface {
|
|
4
|
+
public name: string = "AddCreatedByUserToUser1784048917994";
|
|
5
|
+
|
|
6
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
/*
|
|
8
|
+
* Track who created (invited) a user, when they were invited by another user.
|
|
9
|
+
* Existing users are left NULL; this is only populated going forward.
|
|
10
|
+
*/
|
|
11
|
+
await queryRunner.query(`ALTER TABLE "User" ADD "createdByUserId" uuid`);
|
|
12
|
+
await queryRunner.query(
|
|
13
|
+
`ALTER TABLE "User" ADD CONSTRAINT "FK_cd94f8dd722e4d9e890b68ea262" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
18
|
+
await queryRunner.query(
|
|
19
|
+
`ALTER TABLE "User" DROP CONSTRAINT "FK_cd94f8dd722e4d9e890b68ea262"`,
|
|
20
|
+
);
|
|
21
|
+
await queryRunner.query(`ALTER TABLE "User" DROP COLUMN "createdByUserId"`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -449,6 +449,7 @@ import { AddSentinelInsight1784010274993 } from "./1784010274993-AddSentinelInsi
|
|
|
449
449
|
import { AddSentinelInsightFlags1784010274994 } from "./1784010274994-AddSentinelInsightFlags";
|
|
450
450
|
import { RenameSentinelToAI1784030612266 } from "./1784030612266-RenameSentinelToAI";
|
|
451
451
|
import { MigrationName1784033837629 } from "./1784033837629-MigrationName";
|
|
452
|
+
import { AddCreatedByUserToUser1784048917994 } from "./1784048917994-AddCreatedByUserToUser";
|
|
452
453
|
|
|
453
454
|
export default [
|
|
454
455
|
InitialMigration,
|
|
@@ -902,4 +903,5 @@ export default [
|
|
|
902
903
|
AddSentinelInsightFlags1784010274994,
|
|
903
904
|
RenameSentinelToAI1784030612266,
|
|
904
905
|
MigrationName1784033837629,
|
|
906
|
+
AddCreatedByUserToUser1784048917994,
|
|
905
907
|
];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import UserMiddleware from "./UserAuthorization";
|
|
2
|
+
import ProjectMiddleware from "./ProjectAuthorization";
|
|
2
3
|
import JSONWebToken from "../Utils/JsonWebToken";
|
|
3
4
|
import Response from "../Utils/Response";
|
|
4
5
|
import {
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
} from "../Utils/Express";
|
|
9
10
|
import NotAuthorizedException from "../../Types/Exception/NotAuthorizedException";
|
|
10
11
|
import JSONWebTokenData from "../../Types/JsonWebTokenData";
|
|
12
|
+
import ObjectID from "../../Types/ObjectID";
|
|
11
13
|
|
|
12
14
|
export default class MasterAdminAuthorization {
|
|
13
15
|
public static async isAuthorizedMasterAdminMiddleware(
|
|
@@ -52,4 +54,39 @@ export default class MasterAdminAuthorization {
|
|
|
52
54
|
);
|
|
53
55
|
}
|
|
54
56
|
}
|
|
57
|
+
|
|
58
|
+
/*
|
|
59
|
+
* Same as isAuthorizedMasterAdminMiddleware, but ALSO accepts the instance-wide
|
|
60
|
+
* master API key (Admin Dashboard → Settings → API Key) supplied in the
|
|
61
|
+
* `apikey` header. The master key has root/master-admin access, so this lets
|
|
62
|
+
* automated callers reach the master-admin instance-health endpoints with the
|
|
63
|
+
* key instead of a logged-in master-admin session.
|
|
64
|
+
*
|
|
65
|
+
* Deliberately scoped to the read-only health / diagnostics routes only. It is
|
|
66
|
+
* NOT used on higher-risk master-admin actions (e.g. the read/write query
|
|
67
|
+
* console or broadcast email), which stay on the JWT-only middleware above so
|
|
68
|
+
* that a leaked static key cannot trigger them headlessly.
|
|
69
|
+
*/
|
|
70
|
+
public static async isAuthorizedMasterAdminOrMasterApiKeyMiddleware(
|
|
71
|
+
req: ExpressRequest,
|
|
72
|
+
res: ExpressResponse,
|
|
73
|
+
next: NextFunction,
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
try {
|
|
76
|
+
const apiKey: ObjectID | null = ProjectMiddleware.getApiKey(req);
|
|
77
|
+
|
|
78
|
+
if (apiKey && (await ProjectMiddleware.isMasterApiKey(apiKey))) {
|
|
79
|
+
next();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// Fall through to the master-admin session (JWT) check below.
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return MasterAdminAuthorization.isAuthorizedMasterAdminMiddleware(
|
|
87
|
+
req,
|
|
88
|
+
res,
|
|
89
|
+
next,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
55
92
|
}
|
|
@@ -60,6 +60,43 @@ export default class ProjectMiddleware {
|
|
|
60
60
|
return Boolean(this.getProjectId(req));
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/*
|
|
64
|
+
* Whether the given key is the instance-wide master API key
|
|
65
|
+
* (Admin Dashboard → Settings → API Key) and that key is currently enabled.
|
|
66
|
+
* The master key has root/master-admin access, so this is shared with
|
|
67
|
+
* MasterAdminAuthorization to let the key reach master-admin-only endpoints
|
|
68
|
+
* (e.g. instance health) as well.
|
|
69
|
+
*/
|
|
70
|
+
@CaptureSpan()
|
|
71
|
+
public static async isMasterApiKey(apiKey: ObjectID): Promise<boolean> {
|
|
72
|
+
/*
|
|
73
|
+
* masterApiKey is a Postgres `uuid` column, so a non-UUID header value would
|
|
74
|
+
* make the lookup raise an "invalid input syntax for type uuid" error on
|
|
75
|
+
* every request. Reject it cleanly up front — a malformed key is never the
|
|
76
|
+
* master key anyway — so callers can safely fall through to other auth.
|
|
77
|
+
*/
|
|
78
|
+
if (!ObjectID.isValidUUID(apiKey.toString())) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const masterKeyGlobalConfig: GlobalConfig | null =
|
|
83
|
+
await GlobalConfigService.findOneBy({
|
|
84
|
+
query: {
|
|
85
|
+
_id: ObjectID.getZeroObjectID().toString(),
|
|
86
|
+
isMasterApiKeyEnabled: true,
|
|
87
|
+
masterApiKey: apiKey,
|
|
88
|
+
},
|
|
89
|
+
props: {
|
|
90
|
+
isRoot: true,
|
|
91
|
+
},
|
|
92
|
+
select: {
|
|
93
|
+
_id: true,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return Boolean(masterKeyGlobalConfig);
|
|
98
|
+
}
|
|
99
|
+
|
|
63
100
|
@CaptureSpan()
|
|
64
101
|
public static async isValidProjectIdAndApiKeyMiddleware(
|
|
65
102
|
req: ExpressRequest,
|
|
@@ -124,22 +161,10 @@ export default class ProjectMiddleware {
|
|
|
124
161
|
|
|
125
162
|
if (!apiKeyRow) {
|
|
126
163
|
// check master key.
|
|
127
|
-
const
|
|
128
|
-
await
|
|
129
|
-
query: {
|
|
130
|
-
_id: ObjectID.getZeroObjectID().toString(),
|
|
131
|
-
isMasterApiKeyEnabled: true,
|
|
132
|
-
masterApiKey: apiKey,
|
|
133
|
-
},
|
|
134
|
-
props: {
|
|
135
|
-
isRoot: true,
|
|
136
|
-
},
|
|
137
|
-
select: {
|
|
138
|
-
_id: true,
|
|
139
|
-
},
|
|
140
|
-
});
|
|
164
|
+
const isMasterApiKey: boolean =
|
|
165
|
+
await ProjectMiddleware.isMasterApiKey(apiKey);
|
|
141
166
|
|
|
142
|
-
if (
|
|
167
|
+
if (isMasterApiKey) {
|
|
143
168
|
(req as OneUptimeRequest).userType = UserType.MasterAdmin;
|
|
144
169
|
|
|
145
170
|
// get master admin user
|
|
@@ -147,6 +147,8 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
|
|
|
147
147
|
user = await UserService.createByEmail({
|
|
148
148
|
email,
|
|
149
149
|
name: nameValue ? new Name(nameValue) : undefined,
|
|
150
|
+
// Record who invited this brand-new user, so it can be surfaced later.
|
|
151
|
+
createdByUserId: createBy.props.userId,
|
|
150
152
|
props: {
|
|
151
153
|
isRoot: true,
|
|
152
154
|
},
|
|
@@ -446,6 +446,7 @@ export class Service extends DatabaseService<Model> {
|
|
|
446
446
|
name: Name | undefined;
|
|
447
447
|
isEmailVerified?: boolean;
|
|
448
448
|
generateRandomPassword?: boolean;
|
|
449
|
+
createdByUserId?: ObjectID | undefined;
|
|
449
450
|
props: DatabaseCommonInteractionProps;
|
|
450
451
|
}): Promise<Model> {
|
|
451
452
|
const { email, props } = data;
|
|
@@ -457,6 +458,15 @@ export class Service extends DatabaseService<Model> {
|
|
|
457
458
|
}
|
|
458
459
|
user.isEmailVerified = data.isEmailVerified || false;
|
|
459
460
|
|
|
461
|
+
/*
|
|
462
|
+
* Record who created this user, when they were created on behalf of
|
|
463
|
+
* another user (for example, when invited to a project by a team member).
|
|
464
|
+
* This lets the admin dashboard show who invited a given user.
|
|
465
|
+
*/
|
|
466
|
+
if (data.createdByUserId) {
|
|
467
|
+
user.createdByUserId = data.createdByUserId;
|
|
468
|
+
}
|
|
469
|
+
|
|
460
470
|
if (data.generateRandomPassword) {
|
|
461
471
|
user.password = new HashedString(Text.generateRandomText(20));
|
|
462
472
|
}
|
|
@@ -22,6 +22,7 @@ export default class MonitorCriteriaExpectationBuilder {
|
|
|
22
22
|
|
|
23
23
|
public static describeCriteriaExpectation(
|
|
24
24
|
criteriaFilter: CriteriaFilter,
|
|
25
|
+
options?: { unit?: string | undefined },
|
|
25
26
|
): string | null {
|
|
26
27
|
if (!criteriaFilter.filterType) {
|
|
27
28
|
return null;
|
|
@@ -31,24 +32,33 @@ export default class MonitorCriteriaExpectationBuilder {
|
|
|
31
32
|
|
|
32
33
|
const value: string | number | undefined = criteriaFilter.value;
|
|
33
34
|
|
|
35
|
+
/*
|
|
36
|
+
* Suffix numeric-threshold comparisons with the metric's display unit
|
|
37
|
+
* (e.g. "greater than 5 sec") so the threshold reads in the same unit
|
|
38
|
+
* as the observed value. Only the value-comparison filter types below
|
|
39
|
+
* carry a numeric threshold — the others (empty, boolean, heartbeat
|
|
40
|
+
* windows, …) supply their own wording and get no suffix.
|
|
41
|
+
*/
|
|
42
|
+
const unitSuffix: string = options?.unit ? ` ${options.unit}` : "";
|
|
43
|
+
|
|
34
44
|
switch (criteriaFilter.filterType) {
|
|
35
45
|
case FilterType.GreaterThan:
|
|
36
|
-
expectation = `to be greater than ${value}`;
|
|
46
|
+
expectation = `to be greater than ${value}${unitSuffix}`;
|
|
37
47
|
break;
|
|
38
48
|
case FilterType.GreaterThanOrEqualTo:
|
|
39
|
-
expectation = `to be greater than or equal to ${value}`;
|
|
49
|
+
expectation = `to be greater than or equal to ${value}${unitSuffix}`;
|
|
40
50
|
break;
|
|
41
51
|
case FilterType.LessThan:
|
|
42
|
-
expectation = `to be less than ${value}`;
|
|
52
|
+
expectation = `to be less than ${value}${unitSuffix}`;
|
|
43
53
|
break;
|
|
44
54
|
case FilterType.LessThanOrEqualTo:
|
|
45
|
-
expectation = `to be less than or equal to ${value}`;
|
|
55
|
+
expectation = `to be less than or equal to ${value}${unitSuffix}`;
|
|
46
56
|
break;
|
|
47
57
|
case FilterType.EqualTo:
|
|
48
|
-
expectation = `to equal ${value}`;
|
|
58
|
+
expectation = `to equal ${value}${unitSuffix}`;
|
|
49
59
|
break;
|
|
50
60
|
case FilterType.NotEqualTo:
|
|
51
|
-
expectation = `to not equal ${value}`;
|
|
61
|
+
expectation = `to not equal ${value}${unitSuffix}`;
|
|
52
62
|
break;
|
|
53
63
|
case FilterType.Contains:
|
|
54
64
|
expectation = `to contain ${value}`;
|
|
@@ -53,9 +53,23 @@ export default class MonitorCriteriaMessageBuilder {
|
|
|
53
53
|
dataToProcess: DataToProcess;
|
|
54
54
|
monitorStep: MonitorStep;
|
|
55
55
|
}): string | null {
|
|
56
|
+
/*
|
|
57
|
+
* Resolve the metric's display unit (undefined for non-metric criteria)
|
|
58
|
+
* so the threshold in the expectation clause reads in the same unit as
|
|
59
|
+
* the observed value — "recorded latest 0.06 sec (expected to be greater
|
|
60
|
+
* than 5 sec)" rather than the unitless "0.06 (expected ... 5)".
|
|
61
|
+
*/
|
|
62
|
+
const metricDisplayUnit: string | undefined =
|
|
63
|
+
MonitorCriteriaObservationBuilder.getMetricValueDisplayUnit({
|
|
64
|
+
criteriaFilter: input.criteriaFilter,
|
|
65
|
+
dataToProcess: input.dataToProcess,
|
|
66
|
+
monitorStep: input.monitorStep,
|
|
67
|
+
});
|
|
68
|
+
|
|
56
69
|
const expectation: string | null =
|
|
57
70
|
MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(
|
|
58
71
|
input.criteriaFilter,
|
|
72
|
+
{ unit: metricDisplayUnit },
|
|
59
73
|
);
|
|
60
74
|
|
|
61
75
|
const observation: string | null =
|
|
@@ -136,7 +136,10 @@ export default class MonitorCriteriaMessageFormatter {
|
|
|
136
136
|
return null;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
public static summarizeNumericSeries(
|
|
139
|
+
public static summarizeNumericSeries(
|
|
140
|
+
values: Array<number>,
|
|
141
|
+
unit?: string | undefined,
|
|
142
|
+
): string | null {
|
|
140
143
|
if (!values.length) {
|
|
141
144
|
return null;
|
|
142
145
|
}
|
|
@@ -147,12 +150,18 @@ export default class MonitorCriteriaMessageFormatter {
|
|
|
147
150
|
return null;
|
|
148
151
|
}
|
|
149
152
|
|
|
153
|
+
/*
|
|
154
|
+
* Suffix each value with its unit (e.g. "0.06 sec") so the reader knows
|
|
155
|
+
* what the numbers mean. Empty when the metric has no known unit.
|
|
156
|
+
*/
|
|
157
|
+
const unitSuffix: string = unit ? ` ${unit}` : "";
|
|
158
|
+
|
|
150
159
|
const latestFormatted: string | null =
|
|
151
160
|
MonitorCriteriaMessageFormatter.formatNumber(latest, {
|
|
152
161
|
maximumFractionDigits: 2,
|
|
153
162
|
});
|
|
154
163
|
|
|
155
|
-
let summary: string = `latest ${latestFormatted ?? latest}`;
|
|
164
|
+
let summary: string = `latest ${latestFormatted ?? latest}${unitSuffix}`;
|
|
156
165
|
|
|
157
166
|
if (values.length > 1) {
|
|
158
167
|
const min: number = Math.min(...values);
|
|
@@ -167,7 +176,9 @@ export default class MonitorCriteriaMessageFormatter {
|
|
|
167
176
|
maximumFractionDigits: 2,
|
|
168
177
|
});
|
|
169
178
|
|
|
170
|
-
summary += ` (min ${minFormatted ?? min}, max ${
|
|
179
|
+
summary += ` (min ${minFormatted ?? min}${unitSuffix}, max ${
|
|
180
|
+
maxFormatted ?? max
|
|
181
|
+
}${unitSuffix})`;
|
|
171
182
|
}
|
|
172
183
|
|
|
173
184
|
summary += ` across ${values.length} data point${
|
|
@@ -1169,8 +1169,25 @@ export default class MonitorCriteriaObservationBuilder {
|
|
|
1169
1169
|
alias: metricValues.alias,
|
|
1170
1170
|
});
|
|
1171
1171
|
|
|
1172
|
+
/*
|
|
1173
|
+
* The display values are expressed in the threshold's unit (or the
|
|
1174
|
+
* metric's native/legend unit when no threshold unit was set). Surface
|
|
1175
|
+
* that unit alongside the numbers so the message reads "latest 0.06 sec"
|
|
1176
|
+
* instead of the unitless "latest 0.06".
|
|
1177
|
+
*/
|
|
1178
|
+
const displayUnit: string | undefined =
|
|
1179
|
+
MonitorCriteriaObservationBuilder.resolveMetricUnits({
|
|
1180
|
+
criteriaFilter: input.criteriaFilter,
|
|
1181
|
+
dataToProcess: input.dataToProcess,
|
|
1182
|
+
monitorStep: input.monitorStep,
|
|
1183
|
+
alias: metricValues.alias,
|
|
1184
|
+
}).displayUnit;
|
|
1185
|
+
|
|
1172
1186
|
const summary: string | null =
|
|
1173
|
-
MonitorCriteriaMessageFormatter.summarizeNumericSeries(
|
|
1187
|
+
MonitorCriteriaMessageFormatter.summarizeNumericSeries(
|
|
1188
|
+
displayValues,
|
|
1189
|
+
displayUnit,
|
|
1190
|
+
);
|
|
1174
1191
|
|
|
1175
1192
|
if (!summary) {
|
|
1176
1193
|
return null;
|
|
@@ -1195,18 +1212,67 @@ export default class MonitorCriteriaObservationBuilder {
|
|
|
1195
1212
|
monitorStep: MonitorStep;
|
|
1196
1213
|
alias: string | null;
|
|
1197
1214
|
}): Array<number> {
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1215
|
+
const { sampleUnit, thresholdUnit } =
|
|
1216
|
+
MonitorCriteriaObservationBuilder.resolveMetricUnits({
|
|
1217
|
+
criteriaFilter: input.criteriaFilter,
|
|
1218
|
+
dataToProcess: input.dataToProcess,
|
|
1219
|
+
monitorStep: input.monitorStep,
|
|
1220
|
+
alias: input.alias,
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1200
1223
|
if (!thresholdUnit) {
|
|
1201
1224
|
return input.values;
|
|
1202
1225
|
}
|
|
1203
1226
|
|
|
1227
|
+
if (!sampleUnit || sampleUnit === thresholdUnit) {
|
|
1228
|
+
return input.values;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
return input.values.map((v: number) => {
|
|
1232
|
+
return MetricUnitUtil.convertToMetricUnit({
|
|
1233
|
+
value: v,
|
|
1234
|
+
fromUnit: sampleUnit,
|
|
1235
|
+
metricUnit: thresholdUnit,
|
|
1236
|
+
});
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/*
|
|
1241
|
+
* Resolve the units involved in a metric-value observation:
|
|
1242
|
+
* - sampleUnit: the unit the raw samples arrive in (legendUnit, or
|
|
1243
|
+
* the metric's native unit when no legend was set).
|
|
1244
|
+
* - thresholdUnit: the unit the user entered the threshold in, if any.
|
|
1245
|
+
* - displayUnit: the unit the rendered numbers (samples + threshold)
|
|
1246
|
+
* are actually expressed in. This mirrors the
|
|
1247
|
+
* MetricMonitorCriteria evaluator: the threshold unit
|
|
1248
|
+
* wins when present (samples are converted into it),
|
|
1249
|
+
* otherwise the samples stay in their native unit.
|
|
1250
|
+
* Every field is best-effort — undefined when the metric response or
|
|
1251
|
+
* config isn't available. Never throws.
|
|
1252
|
+
*/
|
|
1253
|
+
private static resolveMetricUnits(input: {
|
|
1254
|
+
criteriaFilter: CriteriaFilter;
|
|
1255
|
+
dataToProcess: DataToProcess;
|
|
1256
|
+
monitorStep: MonitorStep;
|
|
1257
|
+
alias: string | null;
|
|
1258
|
+
}): {
|
|
1259
|
+
sampleUnit: string | undefined;
|
|
1260
|
+
thresholdUnit: string | undefined;
|
|
1261
|
+
displayUnit: string | undefined;
|
|
1262
|
+
} {
|
|
1263
|
+
const thresholdUnit: string | undefined =
|
|
1264
|
+
input.criteriaFilter.metricMonitorOptions?.thresholdUnit || undefined;
|
|
1265
|
+
|
|
1204
1266
|
const metricResponse: MetricMonitorResponse | null =
|
|
1205
1267
|
MonitorCriteriaDataExtractor.getMetricMonitorResponse(
|
|
1206
1268
|
input.dataToProcess,
|
|
1207
1269
|
);
|
|
1208
1270
|
if (!metricResponse) {
|
|
1209
|
-
return
|
|
1271
|
+
return {
|
|
1272
|
+
sampleUnit: undefined,
|
|
1273
|
+
thresholdUnit,
|
|
1274
|
+
displayUnit: thresholdUnit,
|
|
1275
|
+
};
|
|
1210
1276
|
}
|
|
1211
1277
|
|
|
1212
1278
|
const metricViewConfig: MetricsViewConfig | undefined =
|
|
@@ -1244,17 +1310,69 @@ export default class MonitorCriteriaObservationBuilder {
|
|
|
1244
1310
|
nativeUnitFromMap ||
|
|
1245
1311
|
undefined;
|
|
1246
1312
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1313
|
+
return {
|
|
1314
|
+
sampleUnit,
|
|
1315
|
+
thresholdUnit,
|
|
1316
|
+
displayUnit: MonitorCriteriaObservationBuilder.normalizeDisplayUnit(
|
|
1317
|
+
thresholdUnit || sampleUnit,
|
|
1318
|
+
),
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/*
|
|
1323
|
+
* Suppress units that would read as noise next to a raw number. OTel's
|
|
1324
|
+
* dimensionless "1" marks ratio metrics whose samples are fractions in
|
|
1325
|
+
* [0, 1]; rendering "0.06 1" is both ugly and misleading (it is not 1% —
|
|
1326
|
+
* it is 6%). Returning undefined leaves the number unlabelled, matching
|
|
1327
|
+
* the pre-unit behavior for that specific case. Any real unit passes
|
|
1328
|
+
* through unchanged.
|
|
1329
|
+
*/
|
|
1330
|
+
private static normalizeDisplayUnit(
|
|
1331
|
+
unit: string | undefined,
|
|
1332
|
+
): string | undefined {
|
|
1333
|
+
if (!unit || !unit.trim()) {
|
|
1334
|
+
return undefined;
|
|
1249
1335
|
}
|
|
1250
1336
|
|
|
1251
|
-
|
|
1252
|
-
return
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1337
|
+
if (unit.trim() === "1") {
|
|
1338
|
+
return undefined;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
return unit;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/*
|
|
1345
|
+
* Public accessor for the unit that a metric-value criteria's observed
|
|
1346
|
+
* samples and threshold are displayed in. Returns undefined for
|
|
1347
|
+
* non-metric criteria or when no unit can be resolved, so callers can
|
|
1348
|
+
* safely append it as a suffix. Used by the message builder to label the
|
|
1349
|
+
* threshold in the "expected to be greater than 5 sec" clause so it
|
|
1350
|
+
* matches the "recorded latest 0.06 sec" observation.
|
|
1351
|
+
*/
|
|
1352
|
+
public static getMetricValueDisplayUnit(input: {
|
|
1353
|
+
criteriaFilter: CriteriaFilter;
|
|
1354
|
+
dataToProcess: DataToProcess;
|
|
1355
|
+
monitorStep: MonitorStep;
|
|
1356
|
+
}): string | undefined {
|
|
1357
|
+
if (input.criteriaFilter.checkOn !== CheckOn.MetricValue) {
|
|
1358
|
+
return undefined;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const metricValues: {
|
|
1362
|
+
alias: string | null;
|
|
1363
|
+
values: Array<number>;
|
|
1364
|
+
} | null = MonitorCriteriaDataExtractor.extractMetricValues({
|
|
1365
|
+
criteriaFilter: input.criteriaFilter,
|
|
1366
|
+
dataToProcess: input.dataToProcess,
|
|
1367
|
+
monitorStep: input.monitorStep,
|
|
1257
1368
|
});
|
|
1369
|
+
|
|
1370
|
+
return MonitorCriteriaObservationBuilder.resolveMetricUnits({
|
|
1371
|
+
criteriaFilter: input.criteriaFilter,
|
|
1372
|
+
dataToProcess: input.dataToProcess,
|
|
1373
|
+
monitorStep: input.monitorStep,
|
|
1374
|
+
alias: metricValues?.alias ?? null,
|
|
1375
|
+
}).displayUnit;
|
|
1258
1376
|
}
|
|
1259
1377
|
|
|
1260
1378
|
private static getSnmpResponse(input: {
|