@oneuptime/common 8.0.5514 → 8.0.5544

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 (83) hide show
  1. package/Models/DatabaseModels/AcmeChallenge.ts +3 -0
  2. package/Models/DatabaseModels/Domain.ts +6 -1
  3. package/Server/API/AcmeChallengeAPI.ts +64 -0
  4. package/Server/EnvironmentConfig.ts +54 -0
  5. package/Server/Infrastructure/Queue.ts +12 -16
  6. package/Server/Infrastructure/QueueWorker.ts +2 -6
  7. package/Server/Services/DomainService.ts +48 -40
  8. package/Server/Utils/AnalyticsDatabase/Statement.ts +8 -4
  9. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +47 -15
  10. package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +13 -12
  11. package/Server/Utils/Monitor/MonitorAlert.ts +46 -1
  12. package/Server/Utils/Monitor/MonitorCriteriaDataExtractor.ts +208 -0
  13. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +439 -0
  14. package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +141 -0
  15. package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +88 -0
  16. package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +196 -0
  17. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +1123 -0
  18. package/Server/Utils/Monitor/MonitorIncident.ts +46 -1
  19. package/Server/Utils/Monitor/MonitorLogUtil.ts +44 -0
  20. package/Server/Utils/Monitor/MonitorMetricUtil.ts +482 -0
  21. package/Server/Utils/Monitor/MonitorResource.ts +185 -914
  22. package/Server/Utils/StartServer.ts +14 -6
  23. package/Types/Email.ts +3 -7
  24. package/Types/Monitor/IncomingMonitor/IncomingMonitorRequest.ts +2 -0
  25. package/Types/Monitor/LogMonitor/LogMonitorResponse.ts +2 -0
  26. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +2 -0
  27. package/Types/Monitor/MonitorEvaluationSummary.ts +48 -0
  28. package/Types/Monitor/ServerMonitor/ServerMonitorResponse.ts +2 -0
  29. package/Types/Monitor/TraceMonitor/TraceMonitorResponse.ts +2 -0
  30. package/Types/Probe/ProbeApiIngestResponse.ts +2 -0
  31. package/Types/Probe/ProbeMonitorResponse.ts +2 -0
  32. package/UI/Components/Filters/FiltersForm.tsx +4 -1
  33. package/build/dist/Models/DatabaseModels/AcmeChallenge.js +3 -0
  34. package/build/dist/Models/DatabaseModels/AcmeChallenge.js.map +1 -1
  35. package/build/dist/Models/DatabaseModels/Domain.js +6 -1
  36. package/build/dist/Models/DatabaseModels/Domain.js.map +1 -1
  37. package/build/dist/Server/API/AcmeChallengeAPI.js +39 -0
  38. package/build/dist/Server/API/AcmeChallengeAPI.js.map +1 -0
  39. package/build/dist/Server/EnvironmentConfig.js +44 -0
  40. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  41. package/build/dist/Server/Infrastructure/Queue.js +8 -11
  42. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  43. package/build/dist/Server/Infrastructure/QueueWorker.js +2 -6
  44. package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
  45. package/build/dist/Server/Services/DomainService.js +31 -29
  46. package/build/dist/Server/Services/DomainService.js.map +1 -1
  47. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +2 -1
  48. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  49. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +34 -16
  50. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  51. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +12 -12
  52. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
  53. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +42 -4
  54. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  55. package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js +119 -0
  56. package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js.map +1 -0
  57. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +312 -0
  58. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -0
  59. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +109 -0
  60. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -0
  61. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +45 -0
  62. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -0
  63. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +132 -0
  64. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -0
  65. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +583 -0
  66. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -0
  67. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +42 -4
  68. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  69. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +32 -0
  70. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -0
  71. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +361 -0
  72. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -0
  73. package/build/dist/Server/Utils/Monitor/MonitorResource.js +133 -666
  74. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  75. package/build/dist/Server/Utils/StartServer.js +7 -4
  76. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  77. package/build/dist/Types/Email.js +2 -5
  78. package/build/dist/Types/Email.js.map +1 -1
  79. package/build/dist/Types/Monitor/MonitorEvaluationSummary.js +2 -0
  80. package/build/dist/Types/Monitor/MonitorEvaluationSummary.js.map +1 -0
  81. package/build/dist/UI/Components/Filters/FiltersForm.js +2 -1
  82. package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
  83. package/package.json +5 -5
@@ -4,11 +4,13 @@ import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccess
4
4
  import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
5
5
  import ColumnLength from "../../Types/Database/ColumnLength";
6
6
  import ColumnType from "../../Types/Database/ColumnType";
7
+ import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
7
8
  import TableColumn from "../../Types/Database/TableColumn";
8
9
  import TableColumnType from "../../Types/Database/TableColumnType";
9
10
  import TableMetadata from "../../Types/Database/TableMetadata";
10
11
  import IconProp from "../../Types/Icon/IconProp";
11
12
  import ObjectID from "../../Types/ObjectID";
13
+ import Route from "../../Types/API/Route";
12
14
  import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
13
15
 
14
16
  @TableAccessControl({
@@ -24,6 +26,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
24
26
  icon: IconProp.Lock,
25
27
  tableDescription: "HTTP Challege for Lets Encrypt Certificates",
26
28
  })
29
+ @CrudApiEndpoint(new Route("/acme-challenge"))
27
30
  @Entity({
28
31
  name: "AcmeChallenge",
29
32
  })
@@ -301,7 +301,12 @@ export default class Domain extends BaseModel {
301
301
  public deletedByUserId?: ObjectID = undefined;
302
302
 
303
303
  @ColumnAccessControl({
304
- create: [],
304
+ create: [
305
+ Permission.ProjectOwner,
306
+ Permission.ProjectAdmin,
307
+ Permission.ProjectMember,
308
+ Permission.CreateProjectDomain,
309
+ ],
305
310
  read: [
306
311
  Permission.ProjectOwner,
307
312
  Permission.ProjectAdmin,
@@ -0,0 +1,64 @@
1
+ import AcmeChallenge from "../../Models/DatabaseModels/AcmeChallenge";
2
+ import NotFoundException from "../../Types/Exception/NotFoundException";
3
+ import AcmeChallengeService, {
4
+ Service as AcmeChallengeServiceType,
5
+ } from "../Services/AcmeChallengeService";
6
+ import Express, {
7
+ ExpressRequest,
8
+ ExpressResponse,
9
+ ExpressRouter,
10
+ NextFunction,
11
+ } from "../Utils/Express";
12
+ import Response from "../Utils/Response";
13
+ import BaseAPI from "./BaseAPI";
14
+
15
+ export default class AcmeChallengeAPI extends BaseAPI<
16
+ AcmeChallenge,
17
+ AcmeChallengeServiceType
18
+ > {
19
+ private wellKnownRouter: ExpressRouter;
20
+
21
+ public constructor() {
22
+ super(AcmeChallenge, AcmeChallengeService);
23
+
24
+ this.wellKnownRouter = Express.getRouter();
25
+
26
+ this.wellKnownRouter.get(
27
+ "/acme-challenge/.well-known/:token",
28
+ async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
29
+ try {
30
+ const challenge: AcmeChallenge | null =
31
+ await AcmeChallengeService.findOneBy({
32
+ query: {
33
+ token: req.params["token"] as string,
34
+ },
35
+ select: {
36
+ challenge: true,
37
+ },
38
+ props: {
39
+ isRoot: true,
40
+ },
41
+ });
42
+
43
+ if (!challenge) {
44
+ return next(new NotFoundException("Challenge not found"));
45
+ }
46
+
47
+ return Response.sendTextResponse(
48
+ req,
49
+ res,
50
+ challenge.challenge as string,
51
+ );
52
+ } catch (err) {
53
+ return next(err);
54
+ }
55
+ },
56
+ );
57
+
58
+ this.router.use("/", this.wellKnownRouter);
59
+ }
60
+
61
+ public getWellKnownRouter(): ExpressRouter {
62
+ return this.wellKnownRouter;
63
+ }
64
+ }
@@ -23,6 +23,58 @@ export const getAllEnvVars: () => JSONObject = (): JSONObject => {
23
23
  return process.env;
24
24
  };
25
25
 
26
+ const FRONTEND_ENV_ALLOW_LIST: Array<string> = [
27
+ "NODE_ENV",
28
+ "HTTP_PROTOCOL",
29
+ "HOST",
30
+ "BILLING_ENABLED",
31
+ "BILLING_PUBLIC_KEY",
32
+ "IS_ENTERPRISE_EDITION",
33
+ "STRIPE_PUBLIC_KEY",
34
+ "VAPID_PUBLIC_KEY",
35
+ "VAPID_SUBJECT",
36
+ "VERSION",
37
+ "STATUS_PAGE_CNAME_RECORD",
38
+ "ANALYTICS_KEY",
39
+ "ANALYTICS_HOST",
40
+ "GIT_SHA",
41
+ "APP_VERSION",
42
+ "OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT",
43
+ "OPENTELEMETRY_EXPORTER_OTLP_HEADERS",
44
+ "DISABLE_TELEMETRY",
45
+ "SLACK_APP_CLIENT_ID",
46
+ "MICROSOFT_TEAMS_APP_CLIENT_ID",
47
+ ];
48
+
49
+ const FRONTEND_ENV_ALLOW_PREFIXES: Array<string> = [
50
+ "SUBSCRIPTION_PLAN_",
51
+ "PUBLIC_",
52
+ ];
53
+
54
+ export const getFrontendEnvVars: () => JSONObject = (): JSONObject => {
55
+ const frontendEnv: JSONObject = {};
56
+
57
+ for (const key of Object.keys(process.env)) {
58
+ const shouldInclude: boolean =
59
+ FRONTEND_ENV_ALLOW_LIST.includes(key) ||
60
+ FRONTEND_ENV_ALLOW_PREFIXES.some((prefix: string) => {
61
+ return key.startsWith(prefix);
62
+ });
63
+
64
+ if (!shouldInclude) {
65
+ continue;
66
+ }
67
+
68
+ const value: string | undefined = process.env[key];
69
+
70
+ if (typeof value !== "undefined") {
71
+ frontendEnv[key] = value;
72
+ }
73
+ }
74
+
75
+ return frontendEnv;
76
+ };
77
+
26
78
  const parsePositiveNumberFromEnv: (
27
79
  envKey: string,
28
80
  fallback: number,
@@ -276,6 +328,8 @@ export const HttpProtocol: Protocol =
276
328
 
277
329
  export const Host: string = process.env["HOST"] || "";
278
330
 
331
+ export const ProvisionSsl: boolean = process.env["PROVISION_SSL"] === "true";
332
+
279
333
  export const WorkflowScriptTimeoutInMS: number = process.env[
280
334
  "WORKFLOW_SCRIPT_TIMEOUT_IN_MS"
281
335
  ]
@@ -1,9 +1,4 @@
1
- import {
2
- ClusterKey,
3
- RedisHostname,
4
- RedisPassword,
5
- RedisPort,
6
- } from "../EnvironmentConfig";
1
+ import { ClusterKey } from "../EnvironmentConfig";
7
2
  import Dictionary from "../../Types/Dictionary";
8
3
  import { JSONObject } from "../../Types/JSON";
9
4
  import { Queue as BullQueue, Job, JobsOptions } from "bullmq";
@@ -13,6 +8,7 @@ import { BullMQAdapter } from "@bull-board/api/bullMQAdapter";
13
8
  import { ExpressRouter } from "../Utils/Express";
14
9
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
15
10
  import logger from "../Utils/Logger";
11
+ import Redis from "./Redis";
16
12
 
17
13
  export enum QueueName {
18
14
  Workflow = "Workflow",
@@ -25,6 +21,7 @@ export enum QueueName {
25
21
  }
26
22
 
27
23
  export type QueueJob = Job;
24
+ type BullBoardQueues = Parameters<typeof createBullBoard>[0]["queues"];
28
25
 
29
26
  export default class Queue {
30
27
  private static queueDict: Dictionary<BullQueue> = {};
@@ -82,11 +79,7 @@ export default class Queue {
82
79
  }
83
80
 
84
81
  const queue: BullQueue = new BullQueue(queueName, {
85
- connection: {
86
- host: RedisHostname.toString(),
87
- port: RedisPort.toNumber(),
88
- password: RedisPassword,
89
- },
82
+ connection: Redis.getRedisOptions(),
90
83
  // Keep BullMQ data under control to avoid Redis bloat
91
84
  defaultJobOptions: {
92
85
  // keep only recent completed/failed jobs
@@ -157,12 +150,15 @@ export default class Queue {
157
150
  public static getQueueInspectorRouter(): ExpressRouter {
158
151
  const serverAdapter: ExpressAdapter = new ExpressAdapter();
159
152
 
153
+ const queueAdapters: BullMQAdapter[] = Object.values(QueueName).map(
154
+ (queueName: QueueName) => {
155
+ return new BullMQAdapter(this.getQueue(queueName));
156
+ },
157
+ );
158
+
160
159
  createBullBoard({
161
- queues: [
162
- ...Object.values(QueueName).map((queueName: QueueName) => {
163
- return new BullMQAdapter(this.getQueue(queueName));
164
- }),
165
- ],
160
+ // Cast keeps compatibility until bull-board widens QueueJob.progress
161
+ queues: queueAdapters as unknown as BullBoardQueues,
166
162
  serverAdapter: serverAdapter,
167
163
  });
168
164
 
@@ -1,4 +1,3 @@
1
- import { RedisHostname, RedisPassword, RedisPort } from "../EnvironmentConfig";
2
1
  import { QueueJob, QueueName } from "./Queue";
3
2
  import TimeoutException from "../../Types/Exception/TimeoutException";
4
3
  import {
@@ -8,6 +7,7 @@ import {
8
7
  } from "../../Types/FunctionTypes";
9
8
  import { Worker } from "bullmq";
10
9
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
10
+ import Redis from "./Redis";
11
11
 
12
12
  export default class QueueWorker {
13
13
  @CaptureSpan()
@@ -30,11 +30,7 @@ export default class QueueWorker {
30
30
  },
31
31
  ): Worker {
32
32
  const worker: Worker = new Worker(queueName, onJobInQueue, {
33
- connection: {
34
- host: RedisHostname.toString(),
35
- port: RedisPort.toNumber(),
36
- password: RedisPassword,
37
- },
33
+ connection: Redis.getRedisOptions(),
38
34
  concurrency: options.concurrency,
39
35
  // Only set these values if provided so we do not override BullMQ defaults
40
36
  ...(options.lockDuration ? { lockDuration: options.lockDuration } : {}),
@@ -7,6 +7,9 @@ import BadDataException from "../../Types/Exception/BadDataException";
7
7
  import Text from "../../Types/Text";
8
8
  import Model from "../../Models/DatabaseModels/Domain";
9
9
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
10
+ import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
11
+ import ObjectID from "../../Types/ObjectID";
12
+ import { FindWhere } from "../../Types/BaseDatabase/Query";
10
13
  export class Service extends DatabaseService<Model> {
11
14
  public constructor() {
12
15
  super(Model);
@@ -32,6 +35,12 @@ export class Service extends DatabaseService<Model> {
32
35
  createBy.data.domain = new Domain(domain.trim().toLowerCase());
33
36
  }
34
37
 
38
+ if (!createBy.props.isRoot && createBy.data.isVerified) {
39
+ throw new BadDataException(
40
+ "Domain cannot be verified during creation. Please verify the domain after creation. Please set isVerified to false.",
41
+ );
42
+ }
43
+
35
44
  createBy.data.domainVerificationText =
36
45
  "oneuptime-verification-" + Text.generateRandomText(20);
37
46
  return Promise.resolve({ createBy, carryForward: null });
@@ -41,67 +50,66 @@ export class Service extends DatabaseService<Model> {
41
50
  protected override async onBeforeUpdate(
42
51
  updateBy: UpdateBy<Model>,
43
52
  ): Promise<OnUpdate<Model>> {
44
- if (
45
- updateBy.data.isVerified &&
46
- updateBy.query._id &&
47
- !updateBy.props.isRoot
48
- ) {
53
+ if (updateBy.data.isVerified && !updateBy.props.isRoot) {
54
+ const projectId: FindWhere<ObjectID> | undefined =
55
+ updateBy.query.projectId || updateBy.props.tenantId;
56
+
57
+ if (!projectId) {
58
+ throw new BadDataException(
59
+ "Project ID is required to verify the domain.",
60
+ );
61
+ }
62
+
49
63
  // check the verification of the domain.
50
64
 
51
65
  const items: Array<Model> = await this.findBy({
52
66
  query: {
53
- _id: updateBy.query._id as string,
54
- projectId: updateBy.props.tenantId!,
67
+ projectId,
68
+ ...updateBy.query,
55
69
  },
56
70
  select: {
57
71
  domain: true,
58
72
  domainVerificationText: true,
59
73
  },
60
74
 
61
- limit: 1,
75
+ limit: LIMIT_PER_PROJECT,
62
76
  skip: 0,
63
77
  props: {
64
78
  isRoot: true,
65
79
  },
66
80
  });
67
81
 
68
- if (items.length === 0) {
69
- throw new BadDataException(
70
- "Domain with id " + updateBy.query._id + " not found.",
71
- );
72
- }
82
+ for (const item of items) {
83
+ const domain: string | undefined = item?.domain?.toString();
84
+ const verificationText: string | undefined =
85
+ item?.domainVerificationText?.toString();
73
86
 
74
- const domain: string | undefined = items[0]?.domain?.toString();
75
- const verificationText: string | undefined =
76
- items[0]?.domainVerificationText?.toString();
87
+ if (!domain) {
88
+ throw new BadDataException("Domain not found.");
89
+ }
77
90
 
78
- if (!domain) {
79
- throw new BadDataException(
80
- "Domain with id " + updateBy.query._id + " not found.",
81
- );
82
- }
91
+ if (!verificationText) {
92
+ throw new BadDataException(
93
+ "Domain verification text with id " +
94
+ updateBy.query._id +
95
+ " not found.",
96
+ );
97
+ }
83
98
 
84
- if (!verificationText) {
85
- throw new BadDataException(
86
- "Domain verification text with id " +
87
- updateBy.query._id +
88
- " not found.",
99
+ const isVerified: boolean = await Domain.verifyTxtRecord(
100
+ domain,
101
+ verificationText,
89
102
  );
90
- }
91
-
92
- const isVerified: boolean = await Domain.verifyTxtRecord(
93
- domain,
94
- verificationText,
95
- );
96
103
 
97
- if (!isVerified) {
98
- throw new BadDataException(
99
- "Verification TXT record " +
100
- verificationText +
101
- " not found in domain " +
102
- domain +
103
- ". Please add a TXT record to verify the domain. If you have already added the TXT record, please wait for few hours to let DNS to propagate.",
104
- );
104
+ if (!isVerified) {
105
+ throw new BadDataException(
106
+ "Verification TXT record " +
107
+ verificationText +
108
+ " not found in domain " +
109
+ domain +
110
+ ". Please add a TXT record to verify the domain. If you have already added the TXT record, please wait for few hours to let DNS to propagate.",
111
+ );
112
+ }
105
113
  }
106
114
  }
107
115
 
@@ -177,11 +177,15 @@ export class Statement implements BaseQueryParams {
177
177
  };
178
178
 
179
179
  if ((statementParam as StatementParameter).value instanceof Includes) {
180
- const isNumberArray: boolean = (
180
+ const includesValues: Array<string | number | ObjectID> = (
181
181
  (statementParam as StatementParameter).value as Includes
182
- ).values.every((v: string | number | ObjectID) => {
183
- return typeof v === "number";
184
- });
182
+ ).values as Array<string | number | ObjectID>;
183
+
184
+ const isNumberArray: boolean = includesValues.every(
185
+ (v: string | number | ObjectID) => {
186
+ return typeof v === "number";
187
+ },
188
+ );
185
189
 
186
190
  if (isNumberArray) {
187
191
  return "Array(Int32)";
@@ -563,39 +563,39 @@ export default class CompareCriteria {
563
563
  data.criteriaFilter.filterType !== FilterType.True &&
564
564
  data.criteriaFilter.filterType !== FilterType.False
565
565
  ) {
566
- if (Array.isArray(data.values)) {
567
- message += ` is ${data.values.join(", ")}`;
568
- } else {
569
- message += ` is ${data.values}`;
570
- }
566
+ const formattedValues: string = CompareCriteria.formatCriteriaValues(
567
+ data.values,
568
+ );
569
+
570
+ message += ` is ${formattedValues}`;
571
571
 
572
572
  message += " which is";
573
573
  }
574
574
 
575
575
  switch (data.criteriaFilter.filterType) {
576
576
  case FilterType.GreaterThan:
577
- message += ` greater than ${data.threshold}. `;
577
+ message += ` greater than ${CompareCriteria.formatSingleValue(data.threshold)}. `;
578
578
  break;
579
579
  case FilterType.GreaterThanOrEqualTo:
580
- message += ` greater than or equal to ${data.threshold}. `;
580
+ message += ` greater than or equal to ${CompareCriteria.formatSingleValue(data.threshold)}. `;
581
581
  break;
582
582
  case FilterType.LessThan:
583
- message += ` less than ${data.threshold}. `;
583
+ message += ` less than ${CompareCriteria.formatSingleValue(data.threshold)}. `;
584
584
  break;
585
585
  case FilterType.LessThanOrEqualTo:
586
- message += ` less than or equal to ${data.threshold}. `;
586
+ message += ` less than or equal to ${CompareCriteria.formatSingleValue(data.threshold)}. `;
587
587
  break;
588
588
  case FilterType.NotEqualTo:
589
- message += ` not equal to ${data.threshold}. `;
589
+ message += ` not equal to ${CompareCriteria.formatSingleValue(data.threshold)}. `;
590
590
  break;
591
591
  case FilterType.EqualTo:
592
- message += ` equal to ${data.threshold}. `;
592
+ message += ` equal to ${CompareCriteria.formatSingleValue(data.threshold)}. `;
593
593
  break;
594
594
  case FilterType.Contains:
595
- message += ` contains ${data.threshold}. `;
595
+ message += ` contains ${CompareCriteria.formatSingleValue(data.threshold)}. `;
596
596
  break;
597
597
  case FilterType.NotContains:
598
- message += ` does not contain ${data.threshold}. `;
598
+ message += ` does not contain ${CompareCriteria.formatSingleValue(data.threshold)}. `;
599
599
  break;
600
600
  case FilterType.True:
601
601
  message += ` is ${data.threshold}. `;
@@ -604,13 +604,45 @@ export default class CompareCriteria {
604
604
  message += ` is ${data.threshold}. `;
605
605
  break;
606
606
  case FilterType.StartsWith:
607
- message += ` starts with ${data.threshold}. `;
607
+ message += ` starts with ${CompareCriteria.formatSingleValue(data.threshold)}. `;
608
608
  break;
609
609
  case FilterType.EndsWith:
610
- message += ` ends with ${data.threshold}. `;
610
+ message += ` ends with ${CompareCriteria.formatSingleValue(data.threshold)}. `;
611
611
  break;
612
612
  }
613
613
 
614
614
  return message.trim();
615
615
  }
616
+
617
+ private static formatCriteriaValues(
618
+ values: Array<number | boolean> | number | boolean | string,
619
+ ): string {
620
+ if (Array.isArray(values)) {
621
+ return values
622
+ .map((value: number | boolean) => {
623
+ return CompareCriteria.formatSingleValue(value);
624
+ })
625
+ .join(", ");
626
+ }
627
+
628
+ return CompareCriteria.formatSingleValue(values);
629
+ }
630
+
631
+ private static formatSingleValue(
632
+ value: number | boolean | string | null | undefined,
633
+ ): string {
634
+ if (value === null || value === undefined) {
635
+ return "unknown";
636
+ }
637
+
638
+ if (typeof value === Typeof.Number) {
639
+ return (value as number).toFixed(2);
640
+ }
641
+
642
+ if (typeof value === Typeof.Boolean) {
643
+ return value ? "true" : "false";
644
+ }
645
+
646
+ return value.toString();
647
+ }
616
648
  }
@@ -169,20 +169,21 @@ export default class ServerMonitorCriteria {
169
169
  const diskPath: string =
170
170
  input.criteriaFilter.serverMonitorOptions?.diskPath || "/";
171
171
 
172
- const diskPercent: number =
173
- (
174
- input.dataToProcess as ServerMonitorResponse
175
- ).basicInfrastructureMetrics?.diskMetrics.filter(
176
- (item: BasicDiskMetrics) => {
177
- return (
178
- item.diskPath.trim().toLowerCase() ===
179
- diskPath.trim().toLowerCase()
180
- );
181
- },
182
- )[0]?.percentFree || 0;
172
+ const diskMetric: BasicDiskMetrics | undefined = (
173
+ input.dataToProcess as ServerMonitorResponse
174
+ ).basicInfrastructureMetrics?.diskMetrics.find(
175
+ (item: BasicDiskMetrics) => {
176
+ return (
177
+ item.diskPath.trim().toLowerCase() === diskPath.trim().toLowerCase()
178
+ );
179
+ },
180
+ );
181
+
182
+ const diskUsagePercent: number =
183
+ diskMetric?.percentUsed ?? diskMetric?.percentFree ?? 0;
183
184
 
184
185
  return CompareCriteria.compareCriteriaNumbers({
185
- value: diskPercent,
186
+ value: diskUsagePercent,
186
187
  threshold: threshold as number,
187
188
  criteriaFilter: input.criteriaFilter,
188
189
  });
@@ -21,6 +21,8 @@ import CaptureSpan from "../Telemetry/CaptureSpan";
21
21
  import DataToProcess from "./DataToProcess";
22
22
  import MonitorTemplateUtil from "./MonitorTemplateUtil";
23
23
  import { JSONObject } from "../../../Types/JSON";
24
+ import OneUptimeDate from "../../../Types/Date";
25
+ import MonitorEvaluationSummary from "../../../Types/Monitor/MonitorEvaluationSummary";
24
26
 
25
27
  export default class MonitorAlert {
26
28
  @CaptureSpan()
@@ -30,6 +32,7 @@ export default class MonitorAlert {
30
32
  rootCause: string;
31
33
  criteriaInstance: MonitorCriteriaInstance | null;
32
34
  dataToProcess: DataToProcess;
35
+ evaluationSummary?: MonitorEvaluationSummary | undefined;
33
36
  }): Promise<Array<Alert>> {
34
37
  // check active alerts and if there are open alerts, do not cretae anothr alert.
35
38
  const openAlerts: Array<Alert> = await AlertService.findBy({
@@ -45,6 +48,7 @@ export default class MonitorAlert {
45
48
  _id: true,
46
49
  createdCriteriaId: true,
47
50
  projectId: true,
51
+ alertNumber: true,
48
52
  },
49
53
  props: {
50
54
  isRoot: true,
@@ -68,6 +72,17 @@ export default class MonitorAlert {
68
72
  rootCause: input.rootCause,
69
73
  dataToProcess: input.dataToProcess,
70
74
  });
75
+
76
+ input.evaluationSummary?.events.push({
77
+ type: "alert-resolved",
78
+ title: `Alert resolved: ${openAlert.id?.toString()}`,
79
+ message:
80
+ "Alert auto-resolved because autoresolve is enabled for this criteria.",
81
+ relatedAlertId: openAlert.id?.toString(),
82
+ relatedAlertNumber: openAlert.alertNumber,
83
+ relatedCriteriaId: input.criteriaInstance?.data?.id,
84
+ at: OneUptimeDate.getCurrentDate(),
85
+ });
71
86
  }
72
87
  }
73
88
 
@@ -81,6 +96,7 @@ export default class MonitorAlert {
81
96
  dataToProcess: DataToProcess;
82
97
  rootCause: string;
83
98
  autoResolveCriteriaInstanceIdAlertIdsDictionary: Dictionary<Array<string>>;
99
+ evaluationSummary?: MonitorEvaluationSummary | undefined;
84
100
  props: {
85
101
  telemetryQuery?: TelemetryQuery | undefined;
86
102
  };
@@ -96,6 +112,7 @@ export default class MonitorAlert {
96
112
  rootCause: input.rootCause,
97
113
  criteriaInstance: input.criteriaInstance,
98
114
  dataToProcess: input.dataToProcess,
115
+ evaluationSummary: input.evaluationSummary,
99
116
  });
100
117
 
101
118
  if (input.criteriaInstance.data?.createAlerts) {
@@ -124,6 +141,16 @@ export default class MonitorAlert {
124
141
  );
125
142
 
126
143
  if (hasAlreadyOpenAlert) {
144
+ input.evaluationSummary?.events.push({
145
+ type: "alert-skipped",
146
+ title: `Alert already active: ${criteriaAlert.title}`,
147
+ message:
148
+ "Skipped creating a new alert because an active alert exists for this criteria.",
149
+ relatedCriteriaId: input.criteriaInstance.data?.id,
150
+ relatedAlertId: alreadyOpenAlert?.id?.toString(),
151
+ relatedAlertNumber: alreadyOpenAlert?.alertNumber,
152
+ at: OneUptimeDate.getCurrentDate(),
153
+ });
127
154
  continue;
128
155
  }
129
156
 
@@ -214,15 +241,33 @@ export default class MonitorAlert {
214
241
  }
215
242
 
216
243
  if (DisableAutomaticAlertCreation) {
244
+ input.evaluationSummary?.events.push({
245
+ type: "alert-skipped",
246
+ title: "Alert creation skipped",
247
+ message:
248
+ "Automatic alert creation is disabled by environment configuration.",
249
+ relatedCriteriaId: input.criteriaInstance.data?.id,
250
+ at: OneUptimeDate.getCurrentDate(),
251
+ });
217
252
  return;
218
253
  }
219
254
 
220
- await AlertService.create({
255
+ const createdAlert: Alert = await AlertService.create({
221
256
  data: alert,
222
257
  props: {
223
258
  isRoot: true,
224
259
  },
225
260
  });
261
+
262
+ input.evaluationSummary?.events.push({
263
+ type: "alert-created",
264
+ title: `Alert created: ${createdAlert.title || criteriaAlert.title}`,
265
+ message: `Alert triggered from criteria "${input.criteriaInstance.data?.name || "Unnamed criteria"}".`,
266
+ relatedCriteriaId: input.criteriaInstance.data?.id,
267
+ relatedAlertId: createdAlert.id?.toString(),
268
+ relatedAlertNumber: createdAlert.alertNumber,
269
+ at: OneUptimeDate.getCurrentDate(),
270
+ });
226
271
  }
227
272
  }
228
273
  }