feat: integrate CaptureSpan decorator into various classes for enhanced telemetry tracking

This commit is contained in:
Simon Larsen 2025-03-17 19:59:35 +00:00
parent 7c50a09aff
commit 8972cc4d86
No known key found for this signature in database
GPG key ID: 96C5DCA24769DBCA
18 changed files with 338 additions and 313 deletions

View file

@ -4,6 +4,7 @@ import Includes from "Common/Types/BaseDatabase/Includes";
import LessThan from "Common/Types/BaseDatabase/LessThan";
import ObjectID from "Common/Types/ObjectID";
import { CompareType } from "../../../Types/Database/CompareBase";
import CaptureSpan from "../../Utils/Telemetry/CaptureSpan";
export default class QueryHelper {
@CaptureSpan()

View file

@ -7,6 +7,7 @@ import ProductType from "Common/Types/MeteredPlan/ProductType";
import ObjectID from "Common/Types/ObjectID";
import Project from "Common/Models/DatabaseModels/Project";
import TelemetryUsageBilling from "Common/Models/DatabaseModels/TelemetryUsageBilling";
import CaptureSpan from "../../../Utils/Telemetry/CaptureSpan";
export default class TelemetryMeteredPlan extends ServerMeteredPlan {
private _productType!: ProductType;

View file

@ -21,7 +21,6 @@ import Permission, {
} from "Common/Types/Permission";
import CaptureSpan from "../../../Utils/Telemetry/CaptureSpan";
export default class AccessControlPermission {
@CaptureSpan()
public static async checkAccessControlBlockPermissionByModel<

View file

@ -19,6 +19,7 @@ import ObjectID from "Common/Types/ObjectID";
import Typeof from "Common/Types/Typeof";
import { FindOperator } from "typeorm/find-options/FindOperator";
import { CompareType } from "../../../Types/Database/CompareBase";
import CaptureSpan from "../../Utils/Telemetry/CaptureSpan";
export default class QueryUtil {
@CaptureSpan()

View file

@ -5,6 +5,7 @@ import BaseModel, {
} from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
import { JSONObject } from "Common/Types/JSON";
import Typeof from "Common/Types/Typeof";
import CaptureSpan from "../../Utils/Telemetry/CaptureSpan";
export default class SelectUtil {
@CaptureSpan()

View file

@ -10,7 +10,6 @@ import PositiveNumber from "Common/Types/PositiveNumber";
import Text from "Common/Types/Text";
import ComponentMetadata, { Port } from "Common/Types/Workflow/Component";
import BaseModelComponents from "Common/Types/Workflow/Components/BaseModel";
import CaptureSpan from "../../../../Utils/Telemetry/CaptureSpan";
export default class DeleteManyBaseModel<
TBaseModel extends BaseModel,

View file

@ -14,6 +14,7 @@ import ComponentMetadata, { Port } from "Common/Types/Workflow/Component";
import ComponentID from "Common/Types/Workflow/ComponentID";
import ScheduleComponents from "Common/Types/Workflow/Components/Schedule";
import Workflow from "Common/Models/DatabaseModels/Workflow";
import CaptureSpan from "../../../Utils/Telemetry/CaptureSpan";
export default class WebhookTrigger extends TriggerCode {
public constructor() {

View file

@ -30,6 +30,7 @@ import JSONFunctions from "Common/Types/JSONFunctions";
import AggregateBy, {
AggregateUtil,
} from "../../Types/AnalyticsDatabase/AggregateBy";
import CaptureSpan from "../Telemetry/CaptureSpan";
export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
public model!: TBaseModel;

View file

@ -9,6 +9,7 @@ import BadDataException from "../../Types/Exception/BadDataException";
import ScreenSizeType from "../../Types/ScreenSizeType";
import BrowserType from "../../Types/BrowserType";
import logger from "./Logger";
import CaptureSpan from "./Telemetry/CaptureSpan";
export type Page = PlaywrightPage;
export type Browser = PlaywrightBrowser;

View file

@ -1,5 +1,6 @@
import BadDataException from "Common/Types/Exception/BadDataException";
import CronParser, { CronExpression } from "cron-parser";
import CaptureSpan from "./Telemetry/CaptureSpan";
export default class CronTab {
@CaptureSpan()

View file

@ -1,5 +1,6 @@
import { EncryptionSecret } from "../EnvironmentConfig";
import CryptoJS from "crypto-js";
import CaptureSpan from "./Telemetry/CaptureSpan";
export default class Encryption {
@CaptureSpan()

View file

@ -13,6 +13,7 @@ import UserType from "Common/Types/UserType";
import "ejs";
import express from "express";
import { Server, createServer } from "http";
import CaptureSpan from "./Telemetry/CaptureSpan";
export type RequestHandler = express.RequestHandler;
export type NextFunction = express.NextFunction;

View file

@ -17,7 +17,7 @@ import AcmeCertificate from "Common/Models/DatabaseModels/AcmeCertificate";
import AcmeChallenge from "Common/Models/DatabaseModels/AcmeChallenge";
import acme from "acme-client";
import { Challenge } from "acme-client/types/rfc8555";
import Telemetry, { Span } from "../Telemetry";
import CaptureSpan from "../Telemetry/CaptureSpan";
export default class GreenlockUtil {
@CaptureSpan()
@ -25,145 +25,114 @@ export default class GreenlockUtil {
validateCname: (domain: string) => Promise<boolean>;
notifyDomainRemoved: (domain: string) => Promise<void>;
}): Promise<void> {
return await Telemetry.startActiveSpan<Promise<void>>({
name: "GreenlockUtil.renewAllCertsWhichAreExpiringSoon",
fn: async (span: Span): Promise<void> => {
try {
logger.debug("Renewing all certificates");
// get all certificates which are expiring soon
const certificates: AcmeCertificate[] =
await AcmeCertificateService.findBy({
query: {
expiresAt: QueryHelper.lessThanEqualTo(
OneUptimeDate.addRemoveDays(
OneUptimeDate.getCurrentDate(),
40, // 40 days before expiry
),
),
},
limit: LIMIT_MAX,
skip: 0,
select: {
domain: true,
},
sort: {
expiresAt: SortOrder.Ascending,
},
props: {
isRoot: true,
},
});
logger.debug(
`Found ${certificates.length} certificates which are expiring soon`,
);
// order certificate for each domain
for (const certificate of certificates) {
if (!certificate.domain) {
continue;
}
logger.debug(
`Renewing certificate for domain: ${certificate.domain}`,
);
try {
logger.debug("Renewing all certificates");
// get all certificates which are expiring soon
const certificates: AcmeCertificate[] =
await AcmeCertificateService.findBy({
query: {
expiresAt: QueryHelper.lessThanEqualTo(
OneUptimeDate.addRemoveDays(
OneUptimeDate.getCurrentDate(),
40, // 40 days before expiry
),
),
},
limit: LIMIT_MAX,
skip: 0,
select: {
domain: true,
},
sort: {
expiresAt: SortOrder.Ascending,
},
props: {
isRoot: true,
},
});
logger.debug(
`Found ${certificates.length} certificates which are expiring soon`,
//validate cname
const isValidCname: boolean = await data.validateCname(
certificate.domain,
);
// order certificate for each domain
for (const certificate of certificates) {
if (!certificate.domain) {
continue;
}
if (!isValidCname) {
logger.debug(
`Renewing certificate for domain: ${certificate.domain}`,
`CNAME is not valid for domain: ${certificate.domain}`,
);
try {
//validate cname
const isValidCname: boolean = await data.validateCname(
certificate.domain,
);
// if cname is not valid then remove the domain
await GreenlockUtil.removeDomain(certificate.domain);
await data.notifyDomainRemoved(certificate.domain);
if (!isValidCname) {
logger.debug(
`CNAME is not valid for domain: ${certificate.domain}`,
);
logger.error(
`Cname is not valid for domain: ${certificate.domain}`,
);
} else {
logger.debug(
`CNAME is valid for domain: ${certificate.domain}`,
);
// if cname is not valid then remove the domain
await GreenlockUtil.removeDomain(certificate.domain);
await data.notifyDomainRemoved(certificate.domain);
await GreenlockUtil.orderCert({
domain: certificate.domain,
validateCname: data.validateCname,
});
logger.error(
`Cname is not valid for domain: ${certificate.domain}`,
);
} else {
logger.debug(
`CNAME is valid for domain: ${certificate.domain}`,
);
await GreenlockUtil.orderCert({
domain: certificate.domain,
validateCname: data.validateCname,
});
logger.debug(
`Certificate renewed for domain: ${certificate.domain}`,
);
}
} catch (e) {
logger.error(
`Error renewing certificate for domain: ${certificate.domain}`,
);
logger.error(e);
}
logger.debug(
`Certificate renewed for domain: ${certificate.domain}`,
);
}
Telemetry.endSpan(span);
} catch (e) {
logger.error("Error renewing all certificates");
logger.error(
`Error renewing certificate for domain: ${certificate.domain}`,
);
logger.error(e);
// record exception
Telemetry.recordExceptionMarkSpanAsErrorAndEndSpan({
span,
exception: e,
});
throw e;
}
},
});
}
} catch (e) {
logger.error("Error renewing all certificates");
logger.error(e);
throw e;
}
}
@CaptureSpan()
public static async removeDomain(domain: string): Promise<void> {
return await Telemetry.startActiveSpan<Promise<void>>({
name: "GreenlockUtil.orderCert",
options: {
attributes: {
try {
// remove certificate for this domain.
await AcmeCertificateService.deleteBy({
query: {
domain: domain,
},
},
fn: async (span: Span): Promise<void> => {
try {
// remove certificate for this domain.
await AcmeCertificateService.deleteBy({
query: {
domain: domain,
},
limit: 1,
skip: 0,
props: {
isRoot: true,
},
});
Telemetry.endSpan(span);
} catch (err) {
logger.error(`Error removing domain: ${domain}`);
Telemetry.recordExceptionMarkSpanAsErrorAndEndSpan({
span,
exception: err,
});
throw err;
}
},
});
limit: 1,
skip: 0,
props: {
isRoot: true,
},
});
} catch (err) {
logger.error(`Error removing domain: ${domain}`);
throw err;
}
}
@CaptureSpan()
@ -171,218 +140,201 @@ export default class GreenlockUtil {
domain: string;
validateCname: (domain: string) => Promise<boolean>;
}): Promise<void> {
return await Telemetry.startActiveSpan<Promise<void>>({
name: "GreenlockUtil.orderCert",
options: {
attributes: {
domain: data.domain,
},
},
fn: async (span: Span): Promise<void> => {
try {
logger.debug(
`GreenlockUtil - Ordering certificate for domain: ${data.domain}`,
);
try {
logger.debug(
`GreenlockUtil - Ordering certificate for domain: ${data.domain}`,
);
let { domain } = data;
let { domain } = data;
domain = domain.trim().toLowerCase();
domain = domain.trim().toLowerCase();
const acmeAccountKeyInBase64: string = LetsEncryptAccountKey;
const acmeAccountKeyInBase64: string = LetsEncryptAccountKey;
if (!acmeAccountKeyInBase64) {
throw new ServerException(
"No lets encrypt account key found in environment variables. Please add one.",
if (!acmeAccountKeyInBase64) {
throw new ServerException(
"No lets encrypt account key found in environment variables. Please add one.",
);
}
let acmeAccountKey: string = Buffer.from(
acmeAccountKeyInBase64,
"base64",
).toString();
acmeAccountKey = Text.replaceAll(acmeAccountKey, "\\n", "\n");
//validate cname
logger.debug(`Validating cname for domain: ${domain}`);
const isValidCname: boolean = await data.validateCname(domain);
if (!isValidCname) {
logger.debug(`CNAME is not valid for domain: ${domain}`);
logger.debug(`Removing domain: ${domain}`);
await GreenlockUtil.removeDomain(domain);
logger.error(`Cname is not valid for domain: ${domain}`);
throw new BadDataException(
"Cname is not valid for domain " + domain,
);
}
logger.debug(`Cname is valid for domain: ${domain}`);
const client: acme.Client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.production,
accountKey: acmeAccountKey,
});
const [certificateKey, certificateRequest] =
await acme.crypto.createCsr({
commonName: domain,
});
logger.debug(`Ordering certificate for domain: ${domain}`);
const certificate: string = await client.auto({
csr: certificateRequest,
email: LetsEncryptNotificationEmail.toString(),
termsOfServiceAgreed: true,
challengePriority: ["http-01"], // only http-01 challenge is supported by oneuptime
challengeCreateFn: async (
authz: acme.Authorization,
challenge: Challenge,
keyAuthorization: string,
) => {
// Satisfy challenge here
/* http-01 */
if (challenge.type === "http-01") {
logger.debug(
`Creating challenge for domain: ${authz.identifier.value}`,
);
}
let acmeAccountKey: string = Buffer.from(
acmeAccountKeyInBase64,
"base64",
).toString();
const acmeChallenge: AcmeChallenge = new AcmeChallenge();
acmeChallenge.challenge = keyAuthorization;
acmeChallenge.token = challenge.token;
acmeChallenge.domain = authz.identifier.value;
acmeAccountKey = Text.replaceAll(acmeAccountKey, "\\n", "\n");
//validate cname
logger.debug(`Validating cname for domain: ${domain}`);
const isValidCname: boolean = await data.validateCname(domain);
if (!isValidCname) {
logger.debug(`CNAME is not valid for domain: ${domain}`);
logger.debug(`Removing domain: ${domain}`);
await GreenlockUtil.removeDomain(domain);
logger.error(`Cname is not valid for domain: ${domain}`);
throw new BadDataException(
"Cname is not valid for domain " + domain,
);
}
logger.debug(`Cname is valid for domain: ${domain}`);
const client: acme.Client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.production,
accountKey: acmeAccountKey,
});
const [certificateKey, certificateRequest] =
await acme.crypto.createCsr({
commonName: domain,
});
logger.debug(`Ordering certificate for domain: ${domain}`);
const certificate: string = await client.auto({
csr: certificateRequest,
email: LetsEncryptNotificationEmail.toString(),
termsOfServiceAgreed: true,
challengePriority: ["http-01"], // only http-01 challenge is supported by oneuptime
challengeCreateFn: async (
authz: acme.Authorization,
challenge: Challenge,
keyAuthorization: string,
) => {
// Satisfy challenge here
/* http-01 */
if (challenge.type === "http-01") {
logger.debug(
`Creating challenge for domain: ${authz.identifier.value}`,
);
const acmeChallenge: AcmeChallenge = new AcmeChallenge();
acmeChallenge.challenge = keyAuthorization;
acmeChallenge.token = challenge.token;
acmeChallenge.domain = authz.identifier.value;
await AcmeChallengeService.create({
data: acmeChallenge,
props: {
isRoot: true,
},
});
logger.debug(
`Challenge created for domain: ${authz.identifier.value}`,
);
}
},
challengeRemoveFn: async (
authz: acme.Authorization,
challenge: Challenge,
) => {
// Clean up challenge here
logger.debug(
`Removing challenge for domain: ${authz.identifier.value}`,
);
if (challenge.type === "http-01") {
await AcmeChallengeService.deleteBy({
query: {
domain: authz.identifier.value,
},
limit: 1,
skip: 0,
props: {
isRoot: true,
},
});
}
logger.debug(
`Challenge removed for domain: ${authz.identifier.value}`,
);
},
});
logger.debug(`Certificate ordered for domain: ${domain}`);
// get expires at date from certificate
const cert: acme.CertificateInfo =
acme.crypto.readCertificateInfo(certificate);
const issuedAt: Date = cert.notBefore;
const expiresAt: Date = cert.notAfter;
logger.debug(`Certificate expires at: ${expiresAt}`);
logger.debug(`Certificate issued at: ${issuedAt}`);
// check if the certificate is already in the database.
const existingCertificate: AcmeCertificate | null =
await AcmeCertificateService.findOneBy({
query: {
domain: domain,
},
select: {
_id: true,
},
await AcmeChallengeService.create({
data: acmeChallenge,
props: {
isRoot: true,
},
});
if (existingCertificate) {
logger.debug(`Updating certificate for domain: ${domain}`);
logger.debug(
`Challenge created for domain: ${authz.identifier.value}`,
);
}
},
challengeRemoveFn: async (
authz: acme.Authorization,
challenge: Challenge,
) => {
// Clean up challenge here
// update the certificate
await AcmeCertificateService.updateBy({
logger.debug(
`Removing challenge for domain: ${authz.identifier.value}`,
);
if (challenge.type === "http-01") {
await AcmeChallengeService.deleteBy({
query: {
domain: domain,
domain: authz.identifier.value,
},
limit: 1,
skip: 0,
data: {
certificate: certificate.toString(),
certificateKey: certificateKey.toString(),
issuedAt: issuedAt,
expiresAt: expiresAt,
},
props: {
isRoot: true,
},
});
logger.debug(`Certificate updated for domain: ${domain}`);
} else {
logger.debug(`Creating certificate for domain: ${domain}`);
// create the certificate
const acmeCertificate: AcmeCertificate = new AcmeCertificate();
acmeCertificate.domain = domain;
acmeCertificate.certificate = certificate.toString();
acmeCertificate.certificateKey = certificateKey.toString();
acmeCertificate.issuedAt = issuedAt;
acmeCertificate.expiresAt = expiresAt;
await AcmeCertificateService.create({
data: acmeCertificate,
props: {
isRoot: true,
},
});
logger.debug(`Certificate created for domain: ${domain}`);
}
Telemetry.endSpan(span);
} catch (e) {
logger.error(`Error ordering certificate for domain: ${data.domain}`);
Telemetry.recordExceptionMarkSpanAsErrorAndEndSpan({
span,
exception: e,
});
if (e instanceof Exception) {
throw e;
}
throw new ServerException(
`Unable to order certificate for ${data.domain}. Please contact support at support@oneuptime.com for more information.`,
logger.debug(
`Challenge removed for domain: ${authz.identifier.value}`,
);
}
},
});
},
});
logger.debug(`Certificate ordered for domain: ${domain}`);
// get expires at date from certificate
const cert: acme.CertificateInfo =
acme.crypto.readCertificateInfo(certificate);
const issuedAt: Date = cert.notBefore;
const expiresAt: Date = cert.notAfter;
logger.debug(`Certificate expires at: ${expiresAt}`);
logger.debug(`Certificate issued at: ${issuedAt}`);
// check if the certificate is already in the database.
const existingCertificate: AcmeCertificate | null =
await AcmeCertificateService.findOneBy({
query: {
domain: domain,
},
select: {
_id: true,
},
props: {
isRoot: true,
},
});
if (existingCertificate) {
logger.debug(`Updating certificate for domain: ${domain}`);
// update the certificate
await AcmeCertificateService.updateBy({
query: {
domain: domain,
},
limit: 1,
skip: 0,
data: {
certificate: certificate.toString(),
certificateKey: certificateKey.toString(),
issuedAt: issuedAt,
expiresAt: expiresAt,
},
props: {
isRoot: true,
},
});
logger.debug(`Certificate updated for domain: ${domain}`);
} else {
logger.debug(`Creating certificate for domain: ${domain}`);
// create the certificate
const acmeCertificate: AcmeCertificate = new AcmeCertificate();
acmeCertificate.domain = domain;
acmeCertificate.certificate = certificate.toString();
acmeCertificate.certificateKey = certificateKey.toString();
acmeCertificate.issuedAt = issuedAt;
acmeCertificate.expiresAt = expiresAt;
await AcmeCertificateService.create({
data: acmeCertificate,
props: {
isRoot: true,
},
});
logger.debug(`Certificate created for domain: ${domain}`);
}
} catch (e) {
logger.error(`Error ordering certificate for domain: ${data.domain}`);
if (e instanceof Exception) {
throw e;
}
throw new ServerException(
`Unable to order certificate for ${data.domain}. Please contact support at support@oneuptime.com for more information.`,
);
}
}
}

View file

@ -1,6 +1,7 @@
import CompareCriteria from "./CompareCriteria";
import { CheckOn, CriteriaFilter } from "Common/Types/Monitor/CriteriaFilter";
import CustomCodeMonitorResponse from "Common/Types/Monitor/CustomCodeMonitor/CustomCodeMonitorResponse";
import CaptureSpan from "../../Telemetry/CaptureSpan";
export default class CustomCodeMonitoringCriteria {
@CaptureSpan()

View file

@ -5,7 +5,7 @@ import CompareCriteria from "./CompareCriteria";
import { CheckOn, CriteriaFilter } from "Common/Types/Monitor/CriteriaFilter";
export default class TraceMonitorCriteria {
@CaptureSpan()
@CaptureSpan()
public static async isMonitorInstanceCriteriaFilterMet(input: {
dataToProcess: DataToProcess;
criteriaFilter: CriteriaFilter;

View file

@ -21,6 +21,17 @@ const MonitorPage: LazyExoticComponent<FunctionComponent<ComponentProps>> =
return import("../Pages/Monitor/Monitors");
});
const WorkspaceConnectionSlack: LazyExoticComponent<FunctionComponent<ComponentProps>> =
lazy(() => {
return import("../Pages/Monitor/WorkspaceConnectionSlack");
});
const WorkspaceConnectionTeams: LazyExoticComponent<FunctionComponent<ComponentProps>> =
lazy(() => {
return import("../Pages/Monitor/WorkspaceConnectionMicrosoftTeams");
});
const MonitorViewMetrics: LazyExoticComponent<
FunctionComponent<ComponentProps>
> = lazy(() => {
@ -176,6 +187,7 @@ const MonitorRoutes: FunctionComponent<ComponentProps> = (
</Suspense>
}
/>
<PageRoute
path={MonitorsRoutePath[PageMap.MONITORS_INOPERATIONAL] || ""}
element={
@ -188,6 +200,31 @@ const MonitorRoutes: FunctionComponent<ComponentProps> = (
}
/>
<PageRoute
path={MonitorsRoutePath[PageMap.MONITORS_WORKSPACE_CONNECTION_SLACK] || ""}
element={
<Suspense fallback={Loader}>
<WorkspaceConnectionSlack
{...props}
pageRoute={RouteMap[PageMap.MONITORS_WORKSPACE_CONNECTION_SLACK] as Route}
/>
</Suspense>
}
/>
<PageRoute
path={MonitorsRoutePath[PageMap.MONITORS_WORKSPACE_CONNECTION_MICROSOFT_TEAMS] || ""}
element={
<Suspense fallback={Loader}>
<WorkspaceConnectionTeams
{...props}
pageRoute={RouteMap[PageMap.MONITORS_WORKSPACE_CONNECTION_MICROSOFT_TEAMS] as Route}
/>
</Suspense>
}
/>
<PageRoute
path={MonitorsRoutePath[PageMap.MONITOR_CREATE] || ""}
element={

View file

@ -11,6 +11,18 @@ export function getMonitorBreadcrumbs(path: string): Array<Link> | undefined {
"Monitors",
"Inoperational",
]),
//slack connection
...BuildBreadcrumbLinksByTitles(PageMap.MONITORS_WORKSPACE_CONNECTION_SLACK, [
"Project",
"Monitors",
"Slack",
]),
// ms teams connection
...BuildBreadcrumbLinksByTitles(PageMap.MONITORS_WORKSPACE_CONNECTION_MICROSOFT_TEAMS, [
"Project",
"Monitors",
"Microsoft Teams",
]),
...BuildBreadcrumbLinksByTitles(PageMap.MONITORS_DISABLED, [
"Project",
"Monitors",

View file

@ -12,6 +12,9 @@ export const MonitorsRoutePath: Dictionary<string> = {
[PageMap.MONITORS_DISABLED]: "disabled",
[PageMap.MONITORS_PROBE_DISCONNECTED]: "probe-disconnected",
[PageMap.MONITORS_PROBE_DISABLED]: "probe-disabled",
[PageMap.MONITORS_WORKSPACE_CONNECTION_SLACK]: "workspace-connection-slack",
[PageMap.MONITORS_WORKSPACE_CONNECTION_MICROSOFT_TEAMS]:
"workspace-connection-microsoft-teams",
[PageMap.MONITOR_VIEW]: `${RouteParams.ModelID}`,
[PageMap.MONITOR_VIEW_INTERVAL]: `${RouteParams.ModelID}/interval`,
@ -327,6 +330,18 @@ const RouteMap: Dictionary<Route> = {
}`,
),
[PageMap.MONITORS_WORKSPACE_CONNECTION_SLACK]: new Route(
`/dashboard/${RouteParams.ProjectID}/monitors/${
MonitorsRoutePath[PageMap.MONITORS_WORKSPACE_CONNECTION_SLACK]
}`,
),
[PageMap.MONITORS_WORKSPACE_CONNECTION_MICROSOFT_TEAMS]: new Route(
`/dashboard/${RouteParams.ProjectID}/monitors/${
MonitorsRoutePath[PageMap.MONITORS_WORKSPACE_CONNECTION_MICROSOFT_TEAMS]
}`,
),
[PageMap.MONITOR_CREATE]: new Route(
`/dashboard/${RouteParams.ProjectID}/monitors/${
MonitorsRoutePath[PageMap.MONITOR_CREATE]