mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
1620 lines
46 KiB
TypeScript
1620 lines
46 KiB
TypeScript
import DatabaseConfig from "../DatabaseConfig";
|
|
import {
|
|
AllowedActiveMonitorCountInFreePlan,
|
|
IsBillingEnabled,
|
|
} from "../EnvironmentConfig";
|
|
import { ActiveMonitoringMeteredPlan } from "../Types/Billing/MeteredPlan/AllMeteredPlans";
|
|
import CreateBy from "../Types/Database/CreateBy";
|
|
import { OnCreate, OnDelete, OnUpdate } from "../Types/Database/Hooks";
|
|
import QueryHelper from "../Types/Database/QueryHelper";
|
|
import DatabaseService from "./DatabaseService";
|
|
import MonitorOwnerTeamService from "./MonitorOwnerTeamService";
|
|
import MonitorOwnerUserService from "./MonitorOwnerUserService";
|
|
import MonitorProbeService from "./MonitorProbeService";
|
|
import MonitorStatusService from "./MonitorStatusService";
|
|
import MonitorStatusTimelineService from "./MonitorStatusTimelineService";
|
|
import ProbeService from "./ProbeService";
|
|
import ProjectService, { CurrentPlan } from "./ProjectService";
|
|
import TeamMemberService from "./TeamMemberService";
|
|
import URL from "../../Types/API/URL";
|
|
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
import SortOrder from "../../Types/BaseDatabase/SortOrder";
|
|
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
|
|
import LIMIT_MAX, { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
import { JSONObject } from "../../Types/JSON";
|
|
import MonitorType, {
|
|
MonitorTypeHelper,
|
|
} from "../../Types/Monitor/MonitorType";
|
|
import MonitorSteps from "../../Types/Monitor/MonitorSteps";
|
|
import MonitorStep from "../../Types/Monitor/MonitorStep";
|
|
import ObjectID from "../../Types/ObjectID";
|
|
import PositiveNumber from "../../Types/PositiveNumber";
|
|
import Typeof from "../../Types/Typeof";
|
|
import Model from "../../Models/DatabaseModels/Monitor";
|
|
import MonitorOwnerTeam from "../../Models/DatabaseModels/MonitorOwnerTeam";
|
|
import MonitorOwnerUser from "../../Models/DatabaseModels/MonitorOwnerUser";
|
|
import MonitorProbe from "../../Models/DatabaseModels/MonitorProbe";
|
|
import MonitorStatus from "../../Models/DatabaseModels/MonitorStatus";
|
|
import MonitorStatusTimeline from "../../Models/DatabaseModels/MonitorStatusTimeline";
|
|
import Probe, {
|
|
ProbeConnectionStatus,
|
|
} from "../../Models/DatabaseModels/Probe";
|
|
import User from "../../Models/DatabaseModels/User";
|
|
import Select from "../Types/Database/Select";
|
|
import EmailTemplateType from "../../Types/Email/EmailTemplateType";
|
|
import { EmailEnvelope } from "../../Types/Email/EmailMessage";
|
|
import Markdown, { MarkdownContentType } from "../Types/Markdown";
|
|
import Dictionary from "../../Types/Dictionary";
|
|
import { SMSMessage } from "../../Types/SMS/SMS";
|
|
import { CallRequestMessage } from "../../Types/Call/CallRequest";
|
|
import UserNotificationSettingService from "./UserNotificationSettingService";
|
|
import NotificationSettingEventType from "../../Types/NotificationSetting/NotificationSettingEventType";
|
|
import Query from "../Types/Database/Query";
|
|
import DeleteBy from "../Types/Database/DeleteBy";
|
|
import StatusPageResourceService from "./StatusPageResourceService";
|
|
import Label from "../../Models/DatabaseModels/Label";
|
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
|
|
import NotificationRuleWorkspaceChannel from "../../Types/Workspace/NotificationRules/NotificationRuleWorkspaceChannel";
|
|
import WorkspaceNotificationRuleService, {
|
|
MessageBlocksByWorkspaceType,
|
|
} from "./WorkspaceNotificationRuleService";
|
|
import MonitorWorkspaceMessages from "../Utils/Workspace/WorkspaceMessages/Monitor";
|
|
import MonitorFeedService from "./MonitorFeedService";
|
|
import { MonitorFeedEventType } from "../../Models/DatabaseModels/MonitorFeed";
|
|
import { Gray500, Green500 } from "../../Types/BrandColors";
|
|
import LabelService from "./LabelService";
|
|
import logger from "../Utils/Logger";
|
|
import PushNotificationUtil from "../Utils/PushNotificationUtil";
|
|
import ExceptionMessages from "../../Types/Exception/ExceptionMessages";
|
|
import Project from "../../Models/DatabaseModels/Project";
|
|
import { createWhatsAppMessageFromTemplate } from "../Utils/WhatsAppTemplateUtil";
|
|
import { WhatsAppMessagePayload } from "../../Types/WhatsApp/WhatsAppMessage";
|
|
import MetricService from "./MetricService";
|
|
|
|
export interface MonitorDestinationInfo {
|
|
monitorDestination: string;
|
|
requestType: string;
|
|
monitorType: string;
|
|
}
|
|
|
|
export class Service extends DatabaseService<Model> {
|
|
public constructor() {
|
|
super(Model);
|
|
}
|
|
|
|
public getMonitorDestinationInfo(monitor: Model): MonitorDestinationInfo {
|
|
let monitorDestination: string = "";
|
|
let requestType: string = "";
|
|
const monitorType: MonitorType | undefined = monitor.monitorType;
|
|
|
|
if (monitor.monitorSteps) {
|
|
const monitorSteps: MonitorSteps = monitor.monitorSteps;
|
|
const stepsArray: Array<MonitorStep> =
|
|
monitorSteps.data?.monitorStepsInstanceArray || [];
|
|
|
|
if (stepsArray.length > 0) {
|
|
const firstStep: MonitorStep | undefined = stepsArray[0];
|
|
|
|
// Get monitor destination
|
|
if (firstStep?.data?.monitorDestination) {
|
|
monitorDestination =
|
|
firstStep.data.monitorDestination.toString() || "";
|
|
}
|
|
|
|
// Get request type for API monitors
|
|
if (monitorType === MonitorType.API && firstStep?.data?.requestType) {
|
|
requestType = firstStep.data.requestType;
|
|
}
|
|
|
|
// For port monitors, append port to destination
|
|
if (
|
|
monitorType === MonitorType.Port &&
|
|
firstStep?.data?.monitorDestinationPort
|
|
) {
|
|
const port: string = firstStep.data.monitorDestinationPort.toString();
|
|
if (monitorDestination && port) {
|
|
monitorDestination = `${monitorDestination}:${port}`;
|
|
}
|
|
}
|
|
|
|
// For SNMP monitors, use the hostname from snmpMonitor config
|
|
if (monitorType === MonitorType.SNMP && firstStep?.data?.snmpMonitor) {
|
|
monitorDestination = firstStep.data.snmpMonitor.hostname || "";
|
|
const port: number = firstStep.data.snmpMonitor.port || 161;
|
|
if (monitorDestination && port) {
|
|
monitorDestination = `${monitorDestination}:${port}`;
|
|
}
|
|
}
|
|
|
|
// For DNS monitors, use the queryName from dnsMonitor config
|
|
if (monitorType === MonitorType.DNS && firstStep?.data?.dnsMonitor) {
|
|
monitorDestination = firstStep.data.dnsMonitor.queryName || "";
|
|
if (firstStep.data.dnsMonitor.hostname) {
|
|
monitorDestination = `${monitorDestination} @${firstStep.data.dnsMonitor.hostname}`;
|
|
}
|
|
}
|
|
|
|
// For External Status Page monitors, use the statusPageUrl
|
|
if (
|
|
monitorType === MonitorType.ExternalStatusPage &&
|
|
firstStep?.data?.externalStatusPageMonitor
|
|
) {
|
|
monitorDestination =
|
|
firstStep.data.externalStatusPageMonitor.statusPageUrl || "";
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
monitorDestination,
|
|
requestType,
|
|
monitorType: monitorType || "",
|
|
};
|
|
}
|
|
|
|
public async refreshMonitorCurrentStatus(monitorId: ObjectID): Promise<void> {
|
|
const monitor: Model | null = await this.findOneById({
|
|
id: monitorId,
|
|
select: {
|
|
_id: true,
|
|
currentMonitorStatusId: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
ignoreHooks: true,
|
|
},
|
|
});
|
|
|
|
const lastMonitorStatus: MonitorStatusTimeline | null =
|
|
await MonitorStatusTimelineService.findOneBy({
|
|
query: {
|
|
monitorId: monitorId,
|
|
endsAt: QueryHelper.isNull(),
|
|
},
|
|
select: {
|
|
_id: true,
|
|
monitorStatusId: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!lastMonitorStatus) {
|
|
return;
|
|
}
|
|
if (!lastMonitorStatus.monitorStatusId) {
|
|
return;
|
|
}
|
|
|
|
if (!monitor) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
monitor.currentMonitorStatusId?.toString() !==
|
|
lastMonitorStatus.monitorStatusId.toString()
|
|
) {
|
|
await this.updateOneById({
|
|
id: monitor.id!,
|
|
data: {
|
|
currentMonitorStatusId: lastMonitorStatus.monitorStatusId,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
@CaptureSpan()
|
|
protected override async onBeforeDelete(
|
|
deleteBy: DeleteBy<Model>,
|
|
): Promise<OnDelete<Model>> {
|
|
const monitorsPendingDeletion: Array<Model> = await this.findBy({
|
|
query: deleteBy.query,
|
|
limit: LIMIT_MAX,
|
|
skip: 0,
|
|
select: {
|
|
_id: true,
|
|
projectId: true,
|
|
},
|
|
props: deleteBy.props,
|
|
});
|
|
|
|
for (const monitor of monitorsPendingDeletion) {
|
|
if (!monitor.id) {
|
|
continue;
|
|
}
|
|
|
|
// delete all the status page resources for this monitor.
|
|
await StatusPageResourceService.deleteBy({
|
|
query: {
|
|
monitorId: monitor.id,
|
|
},
|
|
limit: LIMIT_MAX,
|
|
skip: 0,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
const projectId: ObjectID | undefined = monitor.projectId as
|
|
| ObjectID
|
|
| undefined;
|
|
|
|
if (!projectId) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
await WorkspaceNotificationRuleService.archiveWorkspaceChannels({
|
|
projectId: projectId,
|
|
notificationFor: {
|
|
monitorId: monitor.id,
|
|
},
|
|
sendMessageBeforeArchiving: {
|
|
_type: "WorkspacePayloadMarkdown",
|
|
text: `🗑️ This monitor is deleted. The channel is being archived.`,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error(
|
|
`Error while archiving workspace channels for monitor ${monitor.id?.toString()}: ${error}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
return {
|
|
deleteBy,
|
|
carryForward: {
|
|
monitors: monitorsPendingDeletion,
|
|
},
|
|
};
|
|
}
|
|
|
|
@CaptureSpan()
|
|
protected override async onDeleteSuccess(
|
|
onDelete: OnDelete<Model>,
|
|
_itemIdsBeforeDelete: ObjectID[],
|
|
): Promise<OnDelete<Model>> {
|
|
if (onDelete.deleteBy.props.tenantId && IsBillingEnabled) {
|
|
await ActiveMonitoringMeteredPlan.reportQuantityToBillingProvider(
|
|
onDelete.deleteBy.props.tenantId,
|
|
);
|
|
}
|
|
|
|
if (onDelete.carryForward && onDelete.carryForward.monitors) {
|
|
for (const monitor of onDelete.carryForward.monitors as Array<Model>) {
|
|
if (!monitor.projectId || !monitor.id) {
|
|
continue;
|
|
}
|
|
|
|
await MetricService.deleteBy({
|
|
query: {
|
|
projectId: monitor.projectId,
|
|
serviceId: monitor.id,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
return onDelete;
|
|
}
|
|
|
|
@CaptureSpan()
|
|
protected override async onUpdateSuccess(
|
|
onUpdate: OnUpdate<Model>,
|
|
updatedItemIds: ObjectID[],
|
|
): Promise<OnUpdate<Model>> {
|
|
if (
|
|
onUpdate.updateBy.data.currentMonitorStatusId &&
|
|
onUpdate.updateBy.props.tenantId
|
|
) {
|
|
await this.changeMonitorStatus(
|
|
onUpdate.updateBy.props.tenantId as ObjectID,
|
|
updatedItemIds as Array<ObjectID>,
|
|
onUpdate.updateBy.data.currentMonitorStatusId as ObjectID,
|
|
true, // notifyOwners = true
|
|
"This status was changed when the monitor was updated.",
|
|
undefined,
|
|
{
|
|
isRoot: true,
|
|
},
|
|
);
|
|
}
|
|
|
|
if (updatedItemIds.length > 0) {
|
|
for (const monitorId of updatedItemIds) {
|
|
const monitor: Model | null = await this.findOneById({
|
|
id: monitorId,
|
|
select: {
|
|
projectId: true,
|
|
name: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
const projectId: ObjectID = monitor!.projectId!;
|
|
const monitorName: string = monitor!.name!;
|
|
|
|
let shouldAddMonitorFeed: boolean = false;
|
|
let feedInfoInMarkdown: string = `Monitor **[${monitorName}](${(await this.getMonitorLinkInDashboard(projectId!, monitorId!)).toString()}) was updated.**`;
|
|
|
|
const createdByUserId: ObjectID | undefined | null =
|
|
onUpdate.updateBy.props.userId;
|
|
|
|
if (onUpdate.updateBy.data.monitoringInterval) {
|
|
await MonitorProbeService.updateNextPingAtForMonitor({
|
|
monitorId: monitorId,
|
|
});
|
|
}
|
|
|
|
if (onUpdate.updateBy.data.name) {
|
|
// add monitor feed.
|
|
|
|
feedInfoInMarkdown += `\n\n**Name**:
|
|
${onUpdate.updateBy.data.name || "No name provided."}
|
|
`;
|
|
shouldAddMonitorFeed = true;
|
|
}
|
|
|
|
if (onUpdate.updateBy.data.description) {
|
|
// add monitor feed.
|
|
|
|
feedInfoInMarkdown += `\n\n**Monitor Description**:
|
|
${onUpdate.updateBy.data.description || "No description provided."}
|
|
`;
|
|
shouldAddMonitorFeed = true;
|
|
}
|
|
|
|
if (
|
|
onUpdate.updateBy.data.labels &&
|
|
onUpdate.updateBy.data.labels.length > 0 &&
|
|
Array.isArray(onUpdate.updateBy.data.labels)
|
|
) {
|
|
const labelIds: Array<ObjectID> = (
|
|
onUpdate.updateBy.data.labels as any
|
|
)
|
|
.map((label: Label) => {
|
|
if (label._id) {
|
|
return new ObjectID(label._id?.toString());
|
|
}
|
|
|
|
return null;
|
|
})
|
|
.filter((labelId: ObjectID | null) => {
|
|
return labelId !== null;
|
|
});
|
|
|
|
const labels: Array<Label> = await LabelService.findBy({
|
|
query: {
|
|
_id: QueryHelper.any(labelIds),
|
|
},
|
|
select: {
|
|
name: true,
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (labels.length > 0) {
|
|
feedInfoInMarkdown += `\n\n**🏷️ Labels**:
|
|
|
|
${labels
|
|
.map((label: Label) => {
|
|
return `- ${label.name}`;
|
|
})
|
|
.join("\n")}
|
|
`;
|
|
|
|
shouldAddMonitorFeed = true;
|
|
}
|
|
}
|
|
|
|
if (shouldAddMonitorFeed) {
|
|
await MonitorFeedService.createMonitorFeedItem({
|
|
monitorId: monitorId,
|
|
projectId: onUpdate.updateBy.props.tenantId as ObjectID,
|
|
monitorFeedEventType: MonitorFeedEventType.MonitorUpdated,
|
|
displayColor: Gray500,
|
|
feedInfoInMarkdown: feedInfoInMarkdown,
|
|
userId: createdByUserId || undefined,
|
|
workspaceNotification: {
|
|
sendWorkspaceNotification: true,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return onUpdate;
|
|
}
|
|
|
|
public getEnabledMonitorQuery(): Query<Model> {
|
|
return {
|
|
disableActiveMonitoring: false, // do not fetch if disabled is true.
|
|
disableActiveMonitoringBecauseOfManualIncident: false,
|
|
disableActiveMonitoringBecauseOfScheduledMaintenanceEvent: false,
|
|
};
|
|
}
|
|
|
|
@CaptureSpan()
|
|
protected override async onBeforeCreate(
|
|
createBy: CreateBy<Model>,
|
|
): Promise<OnCreate<Model>> {
|
|
if (!createBy.data.monitorType) {
|
|
throw new BadDataException("Monitor type required to create monitor.");
|
|
}
|
|
|
|
if (!Object.values(MonitorType).includes(createBy.data.monitorType)) {
|
|
throw new BadDataException(
|
|
`Invalid monitor type "${
|
|
createBy.data.monitorType
|
|
}". Valid monitor types are ${Object.values(MonitorType).join(", ")}.`,
|
|
);
|
|
}
|
|
|
|
if (IsBillingEnabled && createBy.props.tenantId) {
|
|
const currentPlan: CurrentPlan = await ProjectService.getCurrentPlan(
|
|
createBy.props.tenantId,
|
|
);
|
|
|
|
if (currentPlan.isSubscriptionUnpaid) {
|
|
throw new BadDataException(
|
|
"Your subscription is unpaid. Please update your payment method and pay all the outstanding invoices to add more monitors.",
|
|
);
|
|
}
|
|
|
|
if (
|
|
currentPlan.plan === PlanType.Free &&
|
|
createBy.data.monitorType !== MonitorType.Manual
|
|
) {
|
|
const monitorCount: PositiveNumber = await this.countBy({
|
|
query: {
|
|
projectId: createBy.props.tenantId,
|
|
monitorType: QueryHelper.any(
|
|
MonitorTypeHelper.getActiveMonitorTypes(),
|
|
),
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (monitorCount.toNumber() >= AllowedActiveMonitorCountInFreePlan) {
|
|
throw new BadDataException(
|
|
`You have reached the maximum allowed monitor limit for the free plan. Please upgrade your plan to add more monitors.`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (createBy.data.monitorType === MonitorType.Server) {
|
|
createBy.data.serverMonitorSecretKey = ObjectID.generate();
|
|
}
|
|
|
|
if (createBy.data.monitorType === MonitorType.IncomingRequest) {
|
|
createBy.data.incomingRequestSecretKey = ObjectID.generate();
|
|
}
|
|
|
|
if (createBy.data.monitorType === MonitorType.IncomingEmail) {
|
|
createBy.data.incomingEmailSecretKey = ObjectID.generate();
|
|
}
|
|
|
|
if (!createBy.props.tenantId) {
|
|
throw new BadDataException("ProjectId required to create monitor.");
|
|
}
|
|
|
|
const monitorStatus: MonitorStatus | null =
|
|
await MonitorStatusService.findOneBy({
|
|
query: {
|
|
projectId: createBy.props.tenantId,
|
|
isOperationalState: true,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!monitorStatus || !monitorStatus.id) {
|
|
throw new BadDataException(
|
|
"Operational status not found for this project. Please add an operational status",
|
|
);
|
|
}
|
|
|
|
createBy.data.currentMonitorStatusId = monitorStatus.id;
|
|
|
|
return { createBy, carryForward: null };
|
|
}
|
|
|
|
@CaptureSpan()
|
|
protected override async onCreateSuccess(
|
|
onCreate: OnCreate<Model>,
|
|
createdItem: Model,
|
|
): Promise<Model> {
|
|
if (!createdItem.projectId) {
|
|
throw new BadDataException("projectId is required");
|
|
}
|
|
|
|
if (!createdItem.id) {
|
|
throw new BadDataException("id is required");
|
|
}
|
|
|
|
if (!createdItem.currentMonitorStatusId) {
|
|
throw new BadDataException("currentMonitorStatusId is required");
|
|
}
|
|
|
|
const monitor: Model | null = await this.findOneById({
|
|
id: createdItem.id,
|
|
select: {
|
|
projectId: true,
|
|
name: true,
|
|
description: true,
|
|
monitorType: true,
|
|
currentMonitorStatus: {
|
|
name: true,
|
|
},
|
|
labels: {
|
|
name: true,
|
|
},
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
const createdByUserId: ObjectID | undefined | null =
|
|
createdItem.createdByUserId || createdItem.createdByUser?.id;
|
|
|
|
let feedInfoInMarkdown: string = `#### 🌎 Monitor Created:
|
|
|
|
**${createdItem.name?.trim() || "No name provided."}**:
|
|
|
|
${createdItem.description?.trim() || "No description provided."}
|
|
|
|
`;
|
|
|
|
if (monitor?.currentMonitorStatus?.name) {
|
|
feedInfoInMarkdown += `➡️ **Monitor Status**: ${monitor.currentMonitorStatus.name} \n\n`;
|
|
}
|
|
|
|
if (monitor?.monitorType) {
|
|
feedInfoInMarkdown += `⚙️ **Monitor Type**: ${monitor.monitorType} \n\n`;
|
|
}
|
|
|
|
if (monitor?.labels && monitor.labels.length > 0) {
|
|
feedInfoInMarkdown += `🏷️ **Labels**:\n`;
|
|
|
|
for (const label of monitor.labels) {
|
|
feedInfoInMarkdown += `- ${label.name}\n`;
|
|
}
|
|
|
|
feedInfoInMarkdown += `\n\n`;
|
|
}
|
|
|
|
// Execute operations sequentially with error handling (workspace first)
|
|
Promise.resolve()
|
|
.then(async () => {
|
|
try {
|
|
return await this.handleWorkspaceOperationsAsync({
|
|
projectId: createdItem.projectId!,
|
|
monitorId: createdItem.id!,
|
|
monitorName: createdItem.name!,
|
|
feedInfoInMarkdown,
|
|
createdByUserId,
|
|
});
|
|
} catch (error) {
|
|
logger.error(
|
|
"Workspace operations failed in MonitorService.onCreateSuccess",
|
|
);
|
|
logger.error(error as Error);
|
|
return Promise.resolve();
|
|
}
|
|
})
|
|
.then(async () => {
|
|
try {
|
|
return await this.changeMonitorStatus(
|
|
createdItem.projectId!,
|
|
[createdItem.id!],
|
|
createdItem.currentMonitorStatusId!,
|
|
false, // notifyOwners = false
|
|
"This status was created when the monitor was created.",
|
|
undefined,
|
|
onCreate.createBy.props,
|
|
);
|
|
} catch (error) {
|
|
logger.error(
|
|
"Change monitor status failed in MonitorService.onCreateSuccess",
|
|
);
|
|
logger.error(error as Error);
|
|
return Promise.resolve();
|
|
}
|
|
})
|
|
.then(async () => {
|
|
try {
|
|
if (
|
|
createdItem.monitorType &&
|
|
MonitorTypeHelper.isProbableMonitor(createdItem.monitorType)
|
|
) {
|
|
return await this.addDefaultProbesToMonitor(
|
|
createdItem.projectId!,
|
|
createdItem.id!,
|
|
);
|
|
}
|
|
return Promise.resolve();
|
|
} catch (error) {
|
|
logger.error(
|
|
"Add default probes failed in MonitorService.onCreateSuccess",
|
|
);
|
|
logger.error(error as Error);
|
|
return Promise.resolve();
|
|
}
|
|
})
|
|
.then(async () => {
|
|
try {
|
|
if (IsBillingEnabled) {
|
|
return await ActiveMonitoringMeteredPlan.reportQuantityToBillingProvider(
|
|
createdItem.projectId!,
|
|
);
|
|
}
|
|
return Promise.resolve();
|
|
} catch (error) {
|
|
logger.error(
|
|
"Billing operations failed in MonitorService.onCreateSuccess",
|
|
);
|
|
logger.error(error as Error);
|
|
return Promise.resolve();
|
|
}
|
|
})
|
|
.then(async () => {
|
|
try {
|
|
if (
|
|
onCreate.createBy.miscDataProps &&
|
|
(onCreate.createBy.miscDataProps["ownerTeams"] ||
|
|
onCreate.createBy.miscDataProps["ownerUsers"])
|
|
) {
|
|
return await this.addOwners(
|
|
createdItem.projectId!,
|
|
createdItem.id!,
|
|
(onCreate.createBy.miscDataProps[
|
|
"ownerUsers"
|
|
] as Array<ObjectID>) || [],
|
|
(onCreate.createBy.miscDataProps[
|
|
"ownerTeams"
|
|
] as Array<ObjectID>) || [],
|
|
false,
|
|
onCreate.createBy.props,
|
|
);
|
|
}
|
|
return Promise.resolve();
|
|
} catch (error) {
|
|
logger.error("Add owners failed in MonitorService.onCreateSuccess");
|
|
logger.error(error as Error);
|
|
return Promise.resolve();
|
|
}
|
|
})
|
|
.then(async () => {
|
|
try {
|
|
return await this.refreshMonitorProbeStatus(createdItem.id!);
|
|
} catch (error) {
|
|
logger.error(
|
|
"Refresh probe status failed in MonitorService.onCreateSuccess",
|
|
);
|
|
logger.error(error as Error);
|
|
return Promise.resolve();
|
|
}
|
|
})
|
|
.catch((error: Error) => {
|
|
logger.error(
|
|
`Critical error in MonitorService sequential operations: ${error}`,
|
|
);
|
|
});
|
|
|
|
return createdItem;
|
|
}
|
|
|
|
@CaptureSpan()
|
|
private async handleWorkspaceOperationsAsync(data: {
|
|
projectId: ObjectID;
|
|
monitorId: ObjectID;
|
|
monitorName: string;
|
|
feedInfoInMarkdown: string;
|
|
createdByUserId: ObjectID | undefined | null;
|
|
}): Promise<void> {
|
|
// send message to workspaces - slack, teams, etc.
|
|
const workspaceResult: {
|
|
channelsCreated: Array<NotificationRuleWorkspaceChannel>;
|
|
} | null =
|
|
await MonitorWorkspaceMessages.createChannelsAndInviteUsersToChannels({
|
|
projectId: data.projectId,
|
|
monitorId: data.monitorId,
|
|
monitorName: data.monitorName,
|
|
});
|
|
|
|
if (workspaceResult && workspaceResult.channelsCreated?.length > 0) {
|
|
// update monitor with these channels.
|
|
await this.updateOneById({
|
|
id: data.monitorId,
|
|
data: {
|
|
postUpdatesToWorkspaceChannels: workspaceResult.channelsCreated || [],
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
const monitorCreateMessageBlocks: Array<MessageBlocksByWorkspaceType> =
|
|
await MonitorWorkspaceMessages.getMonitorCreateMessageBlocks({
|
|
monitorId: data.monitorId,
|
|
projectId: data.projectId,
|
|
});
|
|
|
|
await MonitorFeedService.createMonitorFeedItem({
|
|
monitorId: data.monitorId,
|
|
projectId: data.projectId,
|
|
monitorFeedEventType: MonitorFeedEventType.MonitorCreated,
|
|
displayColor: Green500,
|
|
feedInfoInMarkdown: data.feedInfoInMarkdown,
|
|
userId: data.createdByUserId || undefined,
|
|
workspaceNotification: {
|
|
appendMessageBlocks: monitorCreateMessageBlocks,
|
|
sendWorkspaceNotification: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async getMonitorLinkInDashboard(
|
|
projectId: ObjectID,
|
|
monitorId: ObjectID,
|
|
): Promise<URL> {
|
|
const dashboardUrl: URL = await DatabaseConfig.getDashboardUrl();
|
|
|
|
return URL.fromString(dashboardUrl.toString()).addRoute(
|
|
`/${projectId.toString()}/monitors/${monitorId.toString()}`,
|
|
);
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async findOwners(monitorId: ObjectID): Promise<Array<User>> {
|
|
if (!monitorId) {
|
|
throw new BadDataException("monitorId is required");
|
|
}
|
|
|
|
const ownerUsers: Array<MonitorOwnerUser> =
|
|
await MonitorOwnerUserService.findBy({
|
|
query: {
|
|
monitorId: monitorId,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
user: {
|
|
_id: true,
|
|
email: true,
|
|
name: true,
|
|
timezone: true,
|
|
} as Select<User>,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
});
|
|
|
|
const ownerTeams: Array<MonitorOwnerTeam> =
|
|
await MonitorOwnerTeamService.findBy({
|
|
query: {
|
|
monitorId: monitorId,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
teamId: true,
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
const users: Array<User> =
|
|
ownerUsers.map((ownerUser: MonitorOwnerUser) => {
|
|
return ownerUser.user!;
|
|
}) || [];
|
|
|
|
if (ownerTeams.length > 0) {
|
|
const teamIds: Array<ObjectID> =
|
|
ownerTeams.map((ownerTeam: MonitorOwnerTeam) => {
|
|
return ownerTeam.teamId!;
|
|
}) || [];
|
|
|
|
const teamUsers: Array<User> =
|
|
await TeamMemberService.getUsersInTeams(teamIds);
|
|
|
|
for (const teamUser of teamUsers) {
|
|
//check if the user is already added.
|
|
const isUserAlreadyAdded: User | undefined = users.find(
|
|
(user: User) => {
|
|
return user.id!.toString() === teamUser.id!.toString();
|
|
},
|
|
);
|
|
|
|
if (!isUserAlreadyAdded) {
|
|
users.push(teamUser);
|
|
}
|
|
}
|
|
}
|
|
|
|
return users;
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async addOwners(
|
|
projectId: ObjectID,
|
|
monitorId: ObjectID,
|
|
userIds: Array<ObjectID>,
|
|
teamIds: Array<ObjectID>,
|
|
notifyOwners: boolean,
|
|
props: DatabaseCommonInteractionProps,
|
|
): Promise<void> {
|
|
for (let teamId of teamIds) {
|
|
if (typeof teamId === Typeof.String) {
|
|
teamId = new ObjectID(teamId.toString());
|
|
}
|
|
|
|
const teamOwner: MonitorOwnerTeam = new MonitorOwnerTeam();
|
|
teamOwner.monitorId = monitorId;
|
|
teamOwner.projectId = projectId;
|
|
teamOwner.teamId = teamId;
|
|
teamOwner.isOwnerNotified = !notifyOwners;
|
|
|
|
await MonitorOwnerTeamService.create({
|
|
data: teamOwner,
|
|
props: props,
|
|
});
|
|
}
|
|
|
|
for (let userId of userIds) {
|
|
if (typeof userId === Typeof.String) {
|
|
userId = new ObjectID(userId.toString());
|
|
}
|
|
const teamOwner: MonitorOwnerUser = new MonitorOwnerUser();
|
|
teamOwner.monitorId = monitorId;
|
|
teamOwner.projectId = projectId;
|
|
teamOwner.userId = userId;
|
|
teamOwner.isOwnerNotified = !notifyOwners;
|
|
await MonitorOwnerUserService.create({
|
|
data: teamOwner,
|
|
props: props,
|
|
});
|
|
}
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async addDefaultProbesToMonitor(
|
|
projectId: ObjectID,
|
|
monitorId: ObjectID,
|
|
): Promise<void> {
|
|
// Fetch project to see if global probes should be added automatically.
|
|
const project: Project | null = await ProjectService.findOneById({
|
|
id: projectId,
|
|
select: {
|
|
_id: true,
|
|
doNotAddGlobalProbesByDefaultOnNewMonitors: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
const shouldSkipGlobalProbes: boolean =
|
|
project?.doNotAddGlobalProbesByDefaultOnNewMonitors === true;
|
|
|
|
let globalProbes: Array<Probe> = [];
|
|
|
|
if (!shouldSkipGlobalProbes) {
|
|
globalProbes = await ProbeService.findBy({
|
|
query: {
|
|
isGlobalProbe: true,
|
|
shouldAutoEnableProbeOnNewMonitors: true,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
const projectProbes: Array<Probe> = await ProbeService.findBy({
|
|
query: {
|
|
isGlobalProbe: false,
|
|
shouldAutoEnableProbeOnNewMonitors: true,
|
|
projectId: projectId,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
const totalProbes: Array<Probe> = [...globalProbes, ...projectProbes];
|
|
|
|
if (totalProbes.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Create all monitor probes in parallel for better performance
|
|
const createPromises: Array<Promise<MonitorProbe>> = [];
|
|
|
|
for (const probe of totalProbes) {
|
|
const monitorProbe: MonitorProbe = new MonitorProbe();
|
|
monitorProbe.monitorId = monitorId;
|
|
monitorProbe.probeId = probe.id!;
|
|
monitorProbe.projectId = projectId;
|
|
monitorProbe.isEnabled = true;
|
|
|
|
createPromises.push(
|
|
MonitorProbeService.create({
|
|
data: monitorProbe,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
// Execute all creates in parallel
|
|
await Promise.all(createPromises);
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async refreshMonitorProbeStatus(monitorId: ObjectID): Promise<void> {
|
|
const monitor: Model | null = await this.findOneById({
|
|
id: monitorId,
|
|
select: {
|
|
_id: true,
|
|
monitorType: true,
|
|
isAllProbesDisconnectedFromThisMonitor: true,
|
|
isNoProbeEnabledOnThisMonitor: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!monitor) {
|
|
return;
|
|
}
|
|
|
|
if (!monitor.id) {
|
|
return;
|
|
}
|
|
|
|
const monitorType: MonitorType | undefined = monitor?.monitorType;
|
|
|
|
if (!monitorType) {
|
|
return;
|
|
}
|
|
|
|
const isProbeableMonitor: boolean =
|
|
MonitorTypeHelper.isProbableMonitor(monitorType);
|
|
|
|
if (!isProbeableMonitor) {
|
|
return;
|
|
}
|
|
|
|
// get all the probes for this monitor.
|
|
|
|
const probesForMonitor: Array<MonitorProbe> =
|
|
await MonitorProbeService.findBy({
|
|
query: {
|
|
monitorId: monitorId,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
isEnabled: true,
|
|
projectId: true,
|
|
monitorId: true,
|
|
probeId: true,
|
|
probe: {
|
|
connectionStatus: true,
|
|
isGlobalProbe: true,
|
|
},
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
const enabledProbes: Array<MonitorProbe> = probesForMonitor.filter(
|
|
(probe: MonitorProbe) => {
|
|
return probe.isEnabled;
|
|
},
|
|
);
|
|
|
|
if (probesForMonitor.length === 0 || enabledProbes.length === 0) {
|
|
if (!monitor.isNoProbeEnabledOnThisMonitor) {
|
|
// no probes for this monitor.
|
|
await this.updateOneById({
|
|
id: monitorId,
|
|
data: {
|
|
isNoProbeEnabledOnThisMonitor: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
// notify owners that no probe is enabled.
|
|
|
|
await this.notifyOwnersWhenNoProbeIsEnabled({
|
|
monitorId: monitorId,
|
|
isNoProbesEnabled: true,
|
|
});
|
|
}
|
|
} else if (monitor.isNoProbeEnabledOnThisMonitor) {
|
|
await this.updateOneById({
|
|
id: monitorId,
|
|
data: {
|
|
isNoProbeEnabledOnThisMonitor: false,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
// notify owners that probes are now enabled.
|
|
|
|
await this.notifyOwnersWhenNoProbeIsEnabled({
|
|
monitorId: monitorId,
|
|
isNoProbesEnabled: false,
|
|
});
|
|
}
|
|
|
|
const disconnectedProbes: Array<MonitorProbe> = probesForMonitor.filter(
|
|
(monitorProbe: MonitorProbe) => {
|
|
return (
|
|
monitorProbe.probe?.connectionStatus ===
|
|
ProbeConnectionStatus.Disconnected && monitorProbe.isEnabled
|
|
);
|
|
},
|
|
);
|
|
|
|
if (IsBillingEnabled) {
|
|
// check if these probes are global probes.
|
|
const anyGlobalProbe: boolean = enabledProbes.some(
|
|
(monitorProbe: MonitorProbe) => {
|
|
return monitorProbe.probe?.isGlobalProbe === true;
|
|
},
|
|
);
|
|
|
|
if (anyGlobalProbe) {
|
|
// do not notify if any global probe is disconnected.
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (
|
|
disconnectedProbes.length === enabledProbes.length &&
|
|
enabledProbes.length > 0
|
|
) {
|
|
if (!monitor.isAllProbesDisconnectedFromThisMonitor) {
|
|
// all probes are disconnected.
|
|
await this.updateOneById({
|
|
id: monitorId,
|
|
data: {
|
|
isAllProbesDisconnectedFromThisMonitor: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
await this.notifyOwnersProbesDisconnected({
|
|
monitorId: monitorId,
|
|
isProbeDisconnected: true,
|
|
});
|
|
}
|
|
} else if (monitor.isAllProbesDisconnectedFromThisMonitor) {
|
|
await this.updateOneById({
|
|
id: monitorId,
|
|
data: {
|
|
isAllProbesDisconnectedFromThisMonitor: false,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
await this.notifyOwnersProbesDisconnected({
|
|
monitorId: monitorId,
|
|
isProbeDisconnected: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async getLabelsForMonitors(data: {
|
|
monitorIds: Array<ObjectID>;
|
|
}): Promise<Array<Label>> {
|
|
if (data.monitorIds.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const monitors: Array<Model> = await this.findBy({
|
|
query: {
|
|
_id: QueryHelper.any(data.monitorIds),
|
|
},
|
|
select: {
|
|
_id: true,
|
|
name: true,
|
|
labels: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
});
|
|
|
|
const labels: Array<Label> = [];
|
|
|
|
for (const monitor of monitors) {
|
|
if (monitor.labels) {
|
|
for (const label of monitor.labels) {
|
|
const isLabelAlreadyAdded: boolean = labels.some((l: Label) => {
|
|
return l.id!.toString() === label.id!.toString();
|
|
});
|
|
|
|
if (!isLabelAlreadyAdded) {
|
|
labels.push(label);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return labels;
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async notifyOwnersWhenNoProbeIsEnabled(data: {
|
|
monitorId: ObjectID;
|
|
isNoProbesEnabled: boolean;
|
|
}): Promise<void> {
|
|
const monitor: Model | null = await this.findOneById({
|
|
id: data.monitorId,
|
|
select: {
|
|
_id: true,
|
|
projectId: true,
|
|
name: true,
|
|
project: {
|
|
name: true,
|
|
},
|
|
description: true,
|
|
monitorType: true,
|
|
monitorSteps: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!monitor) {
|
|
return;
|
|
}
|
|
|
|
if (!monitor.id) {
|
|
return;
|
|
}
|
|
|
|
let doesResourceHasOwners: boolean = true;
|
|
|
|
let owners: Array<User> = await this.findOwners(monitor.id!);
|
|
|
|
if (owners.length === 0) {
|
|
doesResourceHasOwners = false;
|
|
|
|
// find project owners.
|
|
owners = await ProjectService.getOwners(monitor.projectId!);
|
|
}
|
|
|
|
if (owners.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const title: string = data.isNoProbesEnabled
|
|
? "No Probes Enabled. This monitor is not being monitored"
|
|
: "Probes Enabled. This monitor is now being monitored.";
|
|
|
|
const enabledStatus: string = data.isNoProbesEnabled
|
|
? "Disabled"
|
|
: "Enabled";
|
|
|
|
// Get monitor destination info using the helper function
|
|
const destinationInfo: MonitorDestinationInfo =
|
|
this.getMonitorDestinationInfo(monitor);
|
|
|
|
const vars: Dictionary<string> = {
|
|
title: title,
|
|
monitorName: monitor.name!,
|
|
currentStatus: enabledStatus,
|
|
projectName: monitor.project!.name!,
|
|
monitorDescription: await Markdown.convertToHTML(
|
|
monitor.description! || "",
|
|
MarkdownContentType.Email,
|
|
),
|
|
monitorViewLink: (
|
|
await this.getMonitorLinkInDashboard(monitor.projectId!, monitor.id!)
|
|
).toString(),
|
|
monitorDestination: destinationInfo.monitorDestination,
|
|
requestType: destinationInfo.requestType,
|
|
monitorType: destinationInfo.monitorType,
|
|
};
|
|
|
|
if (doesResourceHasOwners === true) {
|
|
vars["isOwner"] = "true";
|
|
}
|
|
|
|
for (const owner of owners) {
|
|
// send email to the owner.
|
|
|
|
const emailMessage: EmailEnvelope = {
|
|
templateType: EmailTemplateType.MonitorProbesStatus,
|
|
vars: vars,
|
|
subject: `[Monitor Probes ${enabledStatus}] ${monitor.name!}`,
|
|
};
|
|
|
|
const sms: SMSMessage = {
|
|
message: `This is a message from OneUptime. Probes for monitor ${monitor.name} is ${enabledStatus}. To unsubscribe from this notification go to User Settings in OneUptime Dashboard.`,
|
|
};
|
|
|
|
const callMessage: CallRequestMessage = {
|
|
data: [
|
|
{
|
|
sayMessage: `This is a message from OneUptime. Probes for monitor ${monitor.name} is ${enabledStatus}. To unsubscribe from this notification go to User Settings in OneUptime Dashboard. Good bye.`,
|
|
},
|
|
],
|
|
};
|
|
|
|
const eventType: NotificationSettingEventType =
|
|
NotificationSettingEventType.SEND_MONITOR_NOTIFICATION_WHEN_NO_PROBES_ARE_MONITORING_THE_MONITOR;
|
|
|
|
const whatsAppMessage: WhatsAppMessagePayload =
|
|
createWhatsAppMessageFromTemplate({
|
|
eventType,
|
|
templateVariables: {
|
|
monitor_name: monitor.name!,
|
|
probe_status: enabledStatus,
|
|
monitor_link: vars["monitorViewLink"] || "",
|
|
},
|
|
});
|
|
|
|
await UserNotificationSettingService.sendUserNotification({
|
|
userId: owner.id!,
|
|
projectId: monitor.projectId!,
|
|
emailEnvelope: emailMessage,
|
|
smsMessage: sms,
|
|
callRequestMessage: callMessage,
|
|
pushNotificationMessage:
|
|
PushNotificationUtil.createMonitorProbeStatusNotification({
|
|
title: "OneUptime: Monitor Probe Status",
|
|
body: `Probes for monitor ${monitor.name} is ${enabledStatus}`,
|
|
tag: "monitor-probe-status",
|
|
monitorId: monitor.id!.toString(),
|
|
monitorName: monitor.name!,
|
|
}),
|
|
whatsAppMessage,
|
|
eventType,
|
|
monitorId: monitor.id!,
|
|
});
|
|
}
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async notifyOwnersProbesDisconnected(data: {
|
|
monitorId: ObjectID;
|
|
isProbeDisconnected: boolean;
|
|
}): Promise<void> {
|
|
const monitor: Model | null = await this.findOneById({
|
|
id: data.monitorId,
|
|
select: {
|
|
_id: true,
|
|
projectId: true,
|
|
name: true,
|
|
project: {
|
|
name: true,
|
|
},
|
|
description: true,
|
|
monitorType: true,
|
|
monitorSteps: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!monitor) {
|
|
return;
|
|
}
|
|
|
|
if (!monitor.id) {
|
|
return;
|
|
}
|
|
|
|
let doesResourceHasOwners: boolean = true;
|
|
|
|
let owners: Array<User> = await this.findOwners(monitor.id!);
|
|
|
|
if (owners.length === 0) {
|
|
doesResourceHasOwners = false;
|
|
|
|
// find project owners.
|
|
owners = await ProjectService.getOwners(monitor.projectId!);
|
|
}
|
|
|
|
if (owners.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const status: string = data.isProbeDisconnected
|
|
? "Disconnected"
|
|
: "Connected";
|
|
|
|
// Get monitor destination info using the helper function
|
|
const destinationInfo: MonitorDestinationInfo =
|
|
this.getMonitorDestinationInfo(monitor);
|
|
|
|
const vars: Dictionary<string> = {
|
|
title: `Probes for monitor ${monitor.name} is ${status}.`,
|
|
monitorName: monitor.name!,
|
|
currentStatus: status,
|
|
projectName: monitor.project!.name!,
|
|
monitorDescription: await Markdown.convertToHTML(
|
|
monitor.description! || "",
|
|
MarkdownContentType.Email,
|
|
),
|
|
monitorViewLink: (
|
|
await this.getMonitorLinkInDashboard(monitor.projectId!, monitor.id!)
|
|
).toString(),
|
|
monitorDestination: destinationInfo.monitorDestination,
|
|
requestType: destinationInfo.requestType,
|
|
monitorType: destinationInfo.monitorType,
|
|
};
|
|
|
|
if (doesResourceHasOwners === true) {
|
|
vars["isOwner"] = "true";
|
|
}
|
|
|
|
for (const owner of owners) {
|
|
// send email to the owner.
|
|
|
|
const emailMessage: EmailEnvelope = {
|
|
templateType: EmailTemplateType.MonitorProbesStatus,
|
|
vars: vars,
|
|
subject: `[Monitor Probes ${status}] ${monitor.name!}`,
|
|
};
|
|
|
|
const sms: SMSMessage = {
|
|
message: `This is a message from OneUptime. Probes for monitor ${monitor.name} is ${status}. To unsubscribe from this notification go to User Settings in OneUptime Dashboard.`,
|
|
};
|
|
|
|
const callMessage: CallRequestMessage = {
|
|
data: [
|
|
{
|
|
sayMessage: `This is a message from OneUptime. New monitor was created ${monitor.name}. To unsubscribe from this notification go to User Settings in OneUptime Dashboard. Good bye.`,
|
|
},
|
|
],
|
|
};
|
|
|
|
const eventType: NotificationSettingEventType =
|
|
NotificationSettingEventType.SEND_MONITOR_NOTIFICATION_WHEN_PORBE_STATUS_CHANGES;
|
|
|
|
const whatsAppMessage: WhatsAppMessagePayload =
|
|
createWhatsAppMessageFromTemplate({
|
|
eventType,
|
|
templateVariables: {
|
|
monitor_name: monitor.name!,
|
|
probe_status: status,
|
|
monitor_link: vars["monitorViewLink"] || "",
|
|
},
|
|
});
|
|
|
|
await UserNotificationSettingService.sendUserNotification({
|
|
userId: owner.id!,
|
|
projectId: monitor.projectId!,
|
|
emailEnvelope: emailMessage,
|
|
smsMessage: sms,
|
|
callRequestMessage: callMessage,
|
|
pushNotificationMessage:
|
|
PushNotificationUtil.createMonitorCreatedNotification({
|
|
monitorName: monitor.name!,
|
|
monitorId: monitor.id!.toString(),
|
|
}),
|
|
whatsAppMessage,
|
|
eventType,
|
|
monitorId: monitor.id!,
|
|
});
|
|
}
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async refreshProbeStatus(probeId: ObjectID): Promise<void> {
|
|
// get all the monitors for this probe.
|
|
|
|
const monitorProbes: Array<MonitorProbe> = await MonitorProbeService.findBy(
|
|
{
|
|
query: {
|
|
probeId: probeId,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
isEnabled: true,
|
|
projectId: true,
|
|
monitorId: true,
|
|
monitor: {
|
|
monitorType: true,
|
|
},
|
|
},
|
|
skip: 0,
|
|
limit: LIMIT_PER_PROJECT,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
},
|
|
);
|
|
|
|
if (monitorProbes.length === 0) {
|
|
return;
|
|
}
|
|
|
|
for (const monitorProbe of monitorProbes) {
|
|
await this.refreshMonitorProbeStatus(monitorProbe.monitorId!);
|
|
}
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async changeMonitorStatus(
|
|
projectId: ObjectID,
|
|
monitorIds: Array<ObjectID>,
|
|
monitorStatusId: ObjectID,
|
|
notifyOwners: boolean,
|
|
rootCause: string | undefined,
|
|
statusChangeLog: JSONObject | undefined,
|
|
props: DatabaseCommonInteractionProps,
|
|
startsAt?: Date | undefined,
|
|
): Promise<void> {
|
|
for (const monitorId of monitorIds) {
|
|
// get last monitor status timeline.
|
|
const lastMonitorStatusTimeline: MonitorStatusTimeline | null =
|
|
await MonitorStatusTimelineService.findOneBy({
|
|
query: {
|
|
monitorId: monitorId,
|
|
projectId: projectId,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
monitorStatusId: true,
|
|
},
|
|
sort: {
|
|
startsAt: SortOrder.Descending,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (
|
|
lastMonitorStatusTimeline &&
|
|
lastMonitorStatusTimeline.monitorStatusId &&
|
|
lastMonitorStatusTimeline.monitorStatusId.toString() ===
|
|
monitorStatusId.toString()
|
|
) {
|
|
// status is same as last status. do not create new status timeline.
|
|
continue;
|
|
}
|
|
|
|
const statusTimeline: MonitorStatusTimeline = new MonitorStatusTimeline();
|
|
|
|
statusTimeline.monitorId = monitorId;
|
|
statusTimeline.monitorStatusId = monitorStatusId;
|
|
statusTimeline.projectId = projectId;
|
|
statusTimeline.isOwnerNotified = !notifyOwners;
|
|
|
|
if (statusChangeLog) {
|
|
statusTimeline.statusChangeLog = statusChangeLog;
|
|
}
|
|
if (rootCause) {
|
|
statusTimeline.rootCause = rootCause;
|
|
}
|
|
|
|
if (startsAt) {
|
|
statusTimeline.startsAt = startsAt;
|
|
}
|
|
|
|
await MonitorStatusTimelineService.create({
|
|
data: statusTimeline,
|
|
props: props,
|
|
});
|
|
}
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async getWorkspaceChannelForMonitor(data: {
|
|
monitorId: ObjectID;
|
|
workspaceType?: WorkspaceType | null;
|
|
}): Promise<Array<NotificationRuleWorkspaceChannel>> {
|
|
const monitor: Model | null = await this.findOneById({
|
|
id: data.monitorId,
|
|
select: {
|
|
postUpdatesToWorkspaceChannels: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!monitor) {
|
|
throw new BadDataException(ExceptionMessages.MonitorNotFound);
|
|
}
|
|
|
|
return (monitor.postUpdatesToWorkspaceChannels || []).filter(
|
|
(channel: NotificationRuleWorkspaceChannel) => {
|
|
if (!data.workspaceType) {
|
|
return true;
|
|
}
|
|
|
|
return channel.workspaceType === data.workspaceType;
|
|
},
|
|
);
|
|
}
|
|
|
|
// get monitor name
|
|
@CaptureSpan()
|
|
public async getMonitorName(data: { monitorId: ObjectID }): Promise<string> {
|
|
const { monitorId } = data;
|
|
|
|
const monitor: Model | null = await this.findOneById({
|
|
id: monitorId,
|
|
select: {
|
|
name: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!monitor) {
|
|
throw new BadDataException(ExceptionMessages.MonitorNotFound);
|
|
}
|
|
|
|
return monitor.name || "";
|
|
}
|
|
}
|
|
export default new Service();
|