feat: Implement Synthetic Monitor Runner Service

- Added a new service for executing synthetic monitor scripts.
- Introduced API endpoint for running synthetic monitors.
- Created configuration for synthetic monitor execution parameters.
- Implemented execution logic with retry mechanisms and timeout handling.
- Added support for multiple browser types and screen sizes.
- Integrated logging and error handling for better observability.
- Established child process management for executing scripts in isolation.
- Updated docker-compose configuration to include the new service.
This commit is contained in:
Nawaz Dhandala
2026-03-12 08:20:54 +00:00
parent 9df8513954
commit 4b05546c94
16 changed files with 3197 additions and 2082 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -104,6 +104,8 @@ spec:
value: {{ $val.syntheticMonitorScriptTimeoutInMs | squote }}
- name: PROBE_CUSTOM_CODE_MONITOR_SCRIPT_TIMEOUT_IN_MS
value: {{ $val.customCodeMonitorScriptTimeoutInMs | squote }}
- name: PROBE_SYNTHETIC_RUNNER_URL
value: "http://127.0.0.1:3885"
- name: PROBE_KEY
{{- if $val.key }}
value: {{ $val.key }}
@@ -140,6 +142,52 @@ spec:
resources:
{{- toYaml $val.resources | nindent 12 }}
{{- end }}
- image: {{ include "oneuptime.image" (dict "Values" $.Values "ServiceName" "probe") }}
name: synthetic-runner
{{- if $val.containerSecurityContext }}
securityContext:
{{- toYaml $val.containerSecurityContext | nindent 12 }}
{{- else if $.Values.containerSecurityContext }}
securityContext:
{{- toYaml $.Values.containerSecurityContext | nindent 12 }}
{{- end }}
imagePullPolicy: {{ $.Values.image.pullPolicy }}
command:
- npm
- run
- start:synthetic-runner
env:
- name: LOG_LEVEL
value: {{ $.Values.logLevel }}
- name: PORT
value: "3885"
- name: OPENTELEMETRY_EXPORTER_OTLP_HEADERS
value: {{ $.Values.openTelemetryExporter.headers }}
- name: OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT
value: {{ $.Values.openTelemetryExporter.endpoint }}
- name: PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS
value: {{ $val.syntheticMonitorScriptTimeoutInMs | squote }}
{{- if $val.disableTelemetryCollection }}
- name: DISABLE_TELEMETRY
value: {{ $val.disableTelemetryCollection | quote }}
{{- end }}
{{- if and $val.proxy $val.proxy.httpProxyUrl }}
- name: HTTP_PROXY_URL
value: {{ $val.proxy.httpProxyUrl | squote }}
{{- end }}
{{- if and $val.proxy $val.proxy.httpsProxyUrl }}
- name: HTTPS_PROXY_URL
value: {{ $val.proxy.httpsProxyUrl | squote }}
{{- end }}
{{- if and $val.proxy $val.proxy.noProxy }}
- name: NO_PROXY
value: {{ $val.proxy.noProxy | squote }}
{{- end }}
{{- include "oneuptime.env.oneuptimeSecret" (dict "Values" $.Values "Release" $.Release) | nindent 12 }}
ports:
- containerPort: 3885
protocol: TCP
name: synthetic-runner
{{- if $val.additionalContainers }}
{{ toYaml $val.additionalContainers | nindent 8 }}
{{- end }}

View File

@@ -86,6 +86,10 @@ export const PORT: Port = new Port(
}),
);
export const PROBE_SYNTHETIC_RUNNER_URL: URL = URL.fromString(
process.env["PROBE_SYNTHETIC_RUNNER_URL"] || "http://127.0.0.1:3885",
);
/*
* Proxy configuration for all HTTP/HTTPS requests made by the probe
* HTTP_PROXY_URL: Proxy for HTTP requests

View File

@@ -0,0 +1,45 @@
import ClusterKeyAuthorization from "Common/Server/Middleware/ClusterKeyAuthorization";
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
} from "Common/Server/Utils/Express";
import Response from "Common/Server/Utils/Response";
import BadDataException from "Common/Types/Exception/BadDataException";
import logger from "Common/Server/Utils/Logger";
import { JSONArray } from "Common/Types/JSON";
import SyntheticMonitorProcessRunner from "../Execution/SyntheticMonitorProcessRunner";
import { SyntheticMonitorExecutionRequest } from "../Types/SyntheticMonitorExecution";
const router: ExpressRouter = Express.getRouter();
router.post(
"/run",
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
const request: SyntheticMonitorExecutionRequest =
req.body as SyntheticMonitorExecutionRequest;
if (!request || typeof request.script !== "string") {
throw new BadDataException("Synthetic monitor script is required");
}
const response = await SyntheticMonitorProcessRunner.execute(request);
return Response.sendJsonObjectResponse(req, res, {
results: response.results as unknown as JSONArray,
});
} catch (error: unknown) {
logger.error(error);
return next(error);
}
},
);
export default router;

View File

@@ -0,0 +1,28 @@
import NumberUtil from "Common/Utils/Number";
import Port from "Common/Types/Port";
export const PORT: Port = new Port(
NumberUtil.parseNumberWithDefault({
value: process.env["PORT"],
defaultValue: 3885,
min: 1,
}),
);
export const SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS: number =
NumberUtil.parseNumberWithDefault({
value: process.env["PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS"],
defaultValue: 60000,
min: 1,
});
export const SYNTHETIC_MONITOR_RETRY_DELAY_IN_MS: number = 1000;
export const SYNTHETIC_MONITOR_ATTEMPT_PADDING_IN_MS: number = 30000;
export const SYNTHETIC_MONITOR_CHILD_USER_ID: number = 1000;
export const SYNTHETIC_MONITOR_CHILD_GROUP_ID: number = 1000;
export const SYNTHETIC_MONITOR_CHILD_HOME_DIR: string =
"/tmp/oneuptime-synthetic-runner";

View File

@@ -0,0 +1,83 @@
import SyntheticMonitorExecutor from "./SyntheticMonitorExecutor";
import logger from "Common/Server/Utils/Logger";
import {
SyntheticMonitorExecutionChildMessage,
SyntheticMonitorExecutionRequest,
} from "../Types/SyntheticMonitorExecution";
let hasHandledMessage: boolean = false;
const sendAndExit: (
message: SyntheticMonitorExecutionChildMessage,
exitCode: number,
) => void = (
message: SyntheticMonitorExecutionChildMessage,
exitCode: number,
): void => {
if (process.send) {
process.send(message, (error: Error | null) => {
if (error) {
logger.error(error);
}
process.exit(exitCode);
});
return;
}
process.exit(exitCode);
};
const handleFatalError: (error: unknown) => void = (error: unknown): void => {
sendAndExit(
{
type: "error",
error: {
message:
(error as Error)?.message ||
(error as Error)?.toString() ||
String(error),
stack: (error as Error)?.stack,
},
},
1,
);
};
process.once("message", async (message: unknown): Promise<void> => {
hasHandledMessage = true;
try {
const request: SyntheticMonitorExecutionRequest =
message as SyntheticMonitorExecutionRequest;
const results = await SyntheticMonitorExecutor.execute(request);
sendAndExit(
{
type: "success",
payload: {
results,
},
},
0,
);
} catch (error: unknown) {
handleFatalError(error);
}
});
process.on("uncaughtException", (error: Error): void => {
handleFatalError(error);
});
process.on("unhandledRejection", (error: unknown): void => {
handleFatalError(error);
});
global.setTimeout(() => {
if (!hasHandledMessage) {
handleFatalError(new Error("Synthetic runner child did not receive a job"));
}
}, 10000);

View File

@@ -0,0 +1,639 @@
import {
SYNTHETIC_MONITOR_RETRY_DELAY_IN_MS,
SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS,
} from "../Config";
import BadDataException from "Common/Types/Exception/BadDataException";
import BrowserType from "Common/Types/Monitor/SyntheticMonitors/BrowserType";
import LocalFile from "Common/Server/Utils/LocalFile";
import logger from "Common/Server/Utils/Logger";
import ScreenSizeType from "Common/Types/Monitor/SyntheticMonitors/ScreenSizeType";
import Screenshots from "Common/Types/Monitor/SyntheticMonitors/Screenshot";
import SyntheticMonitorResponse from "Common/Types/Monitor/SyntheticMonitors/SyntheticMonitorResponse";
import axios from "axios";
import crypto from "crypto";
import http from "http";
import https from "https";
import os from "os";
import { Browser, BrowserContext, Page, chromium, firefox } from "playwright";
import { SyntheticMonitorExecutionRequest } from "../Types/SyntheticMonitorExecution";
const MAX_LOG_BYTES: number = 1_000_000;
type AsyncFunctionConstructor = new (
...args: Array<string>
) => (...runtimeArgs: Array<unknown>) => Promise<unknown>;
const AsyncFunctionImpl: AsyncFunctionConstructor = Object.getPrototypeOf(
async function (): Promise<void> {},
).constructor as AsyncFunctionConstructor;
interface BrowserLaunchOptions {
executablePath?: string;
proxy?: {
server: string;
username?: string;
password?: string;
bypass?: string;
};
timeout?: number;
}
interface BrowserSession {
browser: Browser;
context: BrowserContext;
page: Page;
}
interface ScriptReturnValue {
data?: SyntheticMonitorResponse["result"] | undefined;
screenshots?: Record<string, unknown> | undefined;
}
type ConsoleMethod = (...args: Array<unknown>) => void;
interface ScriptConsole {
log: ConsoleMethod;
info: ConsoleMethod;
warn: ConsoleMethod;
error: ConsoleMethod;
debug: ConsoleMethod;
}
export default class SyntheticMonitorExecutor {
public static async execute(
options: SyntheticMonitorExecutionRequest,
): Promise<Array<SyntheticMonitorResponse>> {
const results: Array<SyntheticMonitorResponse> = [];
for (const browserType of options.browserTypes || []) {
for (const screenSizeType of options.screenSizeTypes || []) {
logger.debug(
`Running Synthetic Monitor: ${options.monitorId || "unknown"}, Screen Size: ${screenSizeType}, Browser: ${browserType}`,
);
const result: SyntheticMonitorResponse | null =
await this.executeWithRetry({
monitorId: options.monitorId,
script: options.script,
browserType: browserType,
screenSizeType: screenSizeType,
retryCountOnError: options.retryCountOnError || 0,
});
if (result) {
result.browserType = browserType;
result.screenSizeType = screenSizeType;
results.push(result);
}
}
}
return results;
}
private static async executeWithRetry(options: {
monitorId?: string | undefined;
script: string;
browserType: BrowserType;
screenSizeType: ScreenSizeType;
retryCountOnError: number;
currentRetry?: number | undefined;
}): Promise<SyntheticMonitorResponse | null> {
const currentRetry: number = options.currentRetry || 0;
const maxRetries: number = options.retryCountOnError;
const result: SyntheticMonitorResponse | null =
await this.executeByBrowserAndScreenSize({
script: options.script,
browserType: options.browserType,
screenSizeType: options.screenSizeType,
});
if (result?.scriptError && currentRetry < maxRetries) {
logger.debug(
`Synthetic Monitor script error, retrying (${currentRetry + 1}/${maxRetries}): ${result.scriptError}`,
);
await this.sleep(SYNTHETIC_MONITOR_RETRY_DELAY_IN_MS);
return this.executeWithRetry({
monitorId: options.monitorId,
script: options.script,
browserType: options.browserType,
screenSizeType: options.screenSizeType,
retryCountOnError: maxRetries,
currentRetry: currentRetry + 1,
});
}
return result;
}
private static async executeByBrowserAndScreenSize(options: {
script: string;
browserType: BrowserType;
screenSizeType: ScreenSizeType;
}): Promise<SyntheticMonitorResponse | null> {
const scriptResult: SyntheticMonitorResponse = {
logMessages: [],
scriptError: undefined,
result: undefined,
screenshots: {},
executionTimeInMS: 0,
browserType: options.browserType,
screenSizeType: options.screenSizeType,
};
let browserSession: BrowserSession | null = null;
try {
const startTime: [number, number] = process.hrtime();
browserSession = await SyntheticMonitorExecutor.getPageByBrowserType({
browserType: options.browserType,
screenSizeType: options.screenSizeType,
});
const returnValue: unknown = await this.runScript({
script: options.script,
page: browserSession.page,
browserType: options.browserType,
screenSizeType: options.screenSizeType,
logMessages: scriptResult.logMessages,
});
const endTime: [number, number] = process.hrtime(startTime);
scriptResult.executionTimeInMS = Math.ceil(
(endTime[0] * 1000000000 + endTime[1]) / 1000000,
);
scriptResult.screenshots = this.getScreenshots(returnValue);
scriptResult.result = this.getResultData(returnValue);
} catch (err: unknown) {
logger.error(err);
scriptResult.scriptError =
(err as Error)?.message || (err as Error)?.toString() || String(err);
} finally {
await SyntheticMonitorExecutor.disposeBrowserSession(browserSession);
}
return scriptResult;
}
private static async runScript(data: {
script: string;
page: Page;
browserType: BrowserType;
screenSizeType: ScreenSizeType;
logMessages: Array<string>;
}): Promise<unknown> {
const sandboxConsole: ScriptConsole = this.createConsole(data.logMessages);
const asyncFunction: (...runtimeArgs: Array<unknown>) => Promise<unknown> =
new AsyncFunctionImpl(
"axios",
"page",
"browserType",
"screenSizeType",
"crypto",
"http",
"https",
"console",
"sleep",
`"use strict";\n${data.script}`,
);
let timeoutHandle: NodeJS.Timeout | undefined = undefined;
const executionPromise: Promise<unknown> = asyncFunction(
axios,
data.page,
data.browserType,
data.screenSizeType,
crypto,
http,
https,
sandboxConsole,
this.sleep,
);
const timeoutPromise: Promise<never> = new Promise(
(_resolve: (value: never) => void, reject: (reason: Error) => void) => {
timeoutHandle = global.setTimeout(() => {
reject(new Error("Script execution timed out"));
}, SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS);
},
);
try {
return await Promise.race([executionPromise, timeoutPromise]);
} finally {
if (timeoutHandle) {
global.clearTimeout(timeoutHandle);
}
}
}
private static createConsole(logMessages: Array<string>): ScriptConsole {
let totalLogBytes: number = 0;
const writeLog: ConsoleMethod = (...args: Array<unknown>): void => {
const message: string = args
.map((value: unknown) => {
return this.serializeLogValue(value);
})
.join(" ");
totalLogBytes += message.length;
if (totalLogBytes <= MAX_LOG_BYTES) {
logMessages.push(message);
}
};
return {
log: writeLog,
info: writeLog,
warn: writeLog,
error: writeLog,
debug: writeLog,
};
}
private static serializeLogValue(value: unknown): string {
if (value instanceof Error) {
return value.stack || value.message;
}
if (typeof value === "string") {
return value;
}
try {
return typeof value === "object" ? JSON.stringify(value) : String(value);
} catch {
return String(value);
}
}
private static getResultData(
returnValue: unknown,
): SyntheticMonitorResponse["result"] {
if (!returnValue || typeof returnValue !== "object") {
return undefined;
}
return (returnValue as ScriptReturnValue).data;
}
private static getScreenshots(returnValue: unknown): Screenshots {
const screenshots: Screenshots = {};
if (!returnValue || typeof returnValue !== "object") {
return screenshots;
}
const screenshotValues: Record<string, unknown> | undefined = (
returnValue as ScriptReturnValue
).screenshots;
if (!screenshotValues) {
return screenshots;
}
for (const screenshotName of Object.keys(screenshotValues)) {
const screenshotValue: unknown = screenshotValues[screenshotName];
if (!Buffer.isBuffer(screenshotValue)) {
continue;
}
screenshots[screenshotName] = screenshotValue.toString("base64");
}
return screenshots;
}
private static getViewportHeightAndWidth(options: {
screenSizeType: ScreenSizeType;
}): {
height: number;
width: number;
} {
let viewPortHeight: number = 0;
let viewPortWidth: number = 0;
switch (options.screenSizeType) {
case ScreenSizeType.Desktop:
viewPortHeight = 1080;
viewPortWidth = 1920;
break;
case ScreenSizeType.Mobile:
viewPortHeight = 640;
viewPortWidth = 360;
break;
case ScreenSizeType.Tablet:
viewPortHeight = 768;
viewPortWidth = 1024;
break;
default:
viewPortHeight = 1080;
viewPortWidth = 1920;
break;
}
return {
height: viewPortHeight,
width: viewPortWidth,
};
}
private static getPlaywrightBrowsersPath(): string {
return (
process.env["PLAYWRIGHT_BROWSERS_PATH"] ||
`${os.homedir()}/.cache/ms-playwright`
);
}
public static async getChromeExecutablePath(): Promise<string> {
const browsersPath: string = this.getPlaywrightBrowsersPath();
const doesDirectoryExist: boolean =
await LocalFile.doesDirectoryExist(browsersPath);
if (!doesDirectoryExist) {
throw new BadDataException("Chrome executable path not found.");
}
const directories: string[] =
await LocalFile.getListOfDirectories(browsersPath);
if (directories.length === 0) {
throw new BadDataException("Chrome executable path not found.");
}
const chromeInstallationName: string | undefined = directories.find(
(directory: string) => {
return directory.includes("chromium");
},
);
if (!chromeInstallationName) {
throw new BadDataException("Chrome executable path not found.");
}
const chromeExecutableCandidates: Array<string> = [
`${browsersPath}/${chromeInstallationName}/chrome-linux/chrome`,
`${browsersPath}/${chromeInstallationName}/chrome-linux64/chrome`,
`${browsersPath}/${chromeInstallationName}/chrome64/chrome`,
`${browsersPath}/${chromeInstallationName}/chrome/chrome`,
];
for (const executablePath of chromeExecutableCandidates) {
if (await LocalFile.doesFileExist(executablePath)) {
return executablePath;
}
}
throw new BadDataException("Chrome executable path not found.");
}
public static async getFirefoxExecutablePath(): Promise<string> {
const browsersPath: string = this.getPlaywrightBrowsersPath();
const doesDirectoryExist: boolean =
await LocalFile.doesDirectoryExist(browsersPath);
if (!doesDirectoryExist) {
throw new BadDataException("Firefox executable path not found.");
}
const directories: string[] =
await LocalFile.getListOfDirectories(browsersPath);
if (directories.length === 0) {
throw new BadDataException("Firefox executable path not found.");
}
const firefoxInstallationName: string | undefined = directories.find(
(directory: string) => {
return directory.includes("firefox");
},
);
if (!firefoxInstallationName) {
throw new BadDataException("Firefox executable path not found.");
}
const firefoxExecutableCandidates: Array<string> = [
`${browsersPath}/${firefoxInstallationName}/firefox/firefox`,
`${browsersPath}/${firefoxInstallationName}/firefox-linux64/firefox`,
`${browsersPath}/${firefoxInstallationName}/firefox64/firefox`,
`${browsersPath}/${firefoxInstallationName}/firefox-64/firefox`,
];
for (const executablePath of firefoxExecutableCandidates) {
if (await LocalFile.doesFileExist(executablePath)) {
return executablePath;
}
}
throw new BadDataException("Firefox executable path not found.");
}
private static async getPageByBrowserType(data: {
browserType: BrowserType;
screenSizeType: ScreenSizeType;
}): Promise<BrowserSession> {
const viewport: {
height: number;
width: number;
} = SyntheticMonitorExecutor.getViewportHeightAndWidth({
screenSizeType: data.screenSizeType,
});
const baseOptions: BrowserLaunchOptions = {
timeout: Math.min(SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS, 30000),
};
const proxyOptions: BrowserLaunchOptions["proxy"] | undefined =
this.getBrowserProxyOptions();
if (proxyOptions) {
baseOptions.proxy = proxyOptions;
logger.debug(
`Synthetic Monitor using proxy: ${proxyOptions.server} (HTTPS: ${Boolean(process.env["HTTPS_PROXY_URL"] || process.env["https_proxy"])}, HTTP: ${Boolean(process.env["HTTP_PROXY_URL"] || process.env["http_proxy"])})`,
);
}
if (data.browserType === BrowserType.Chromium) {
const browser: Browser = await chromium.launch({
executablePath: await this.getChromeExecutablePath(),
...baseOptions,
});
const context: BrowserContext = await browser.newContext({
viewport: {
width: viewport.width,
height: viewport.height,
},
});
const page: Page = await context.newPage();
return {
browser,
context,
page,
};
}
if (data.browserType === BrowserType.Firefox) {
const browser: Browser = await firefox.launch({
executablePath: await this.getFirefoxExecutablePath(),
...baseOptions,
});
let context: BrowserContext | null = null;
try {
context = await browser.newContext({
viewport: {
width: viewport.width,
height: viewport.height,
},
});
const page: Page = await context.newPage();
return {
browser,
context,
page,
};
} catch (error: unknown) {
await SyntheticMonitorExecutor.safeCloseBrowserContext(context);
await SyntheticMonitorExecutor.safeCloseBrowser(browser);
throw error;
}
}
throw new BadDataException("Invalid Browser Type.");
}
private static getBrowserProxyOptions():
| BrowserLaunchOptions["proxy"]
| undefined {
const httpsProxyUrl: string | undefined =
process.env["HTTPS_PROXY_URL"] || process.env["https_proxy"] || undefined;
const httpProxyUrl: string | undefined =
process.env["HTTP_PROXY_URL"] || process.env["http_proxy"] || undefined;
const noProxy: string | undefined =
process.env["NO_PROXY"] || process.env["no_proxy"] || undefined;
const proxyUrl: string | undefined = httpsProxyUrl || httpProxyUrl;
if (!proxyUrl) {
return undefined;
}
const proxyOptions: NonNullable<BrowserLaunchOptions["proxy"]> = {
server: proxyUrl,
};
if (noProxy) {
proxyOptions.bypass = noProxy;
}
try {
const parsedUrl: URL = new URL(proxyUrl);
if (parsedUrl.username && parsedUrl.password) {
proxyOptions.username = parsedUrl.username;
proxyOptions.password = parsedUrl.password;
}
} catch (error: unknown) {
logger.warn(`Failed to parse proxy URL for authentication: ${error}`);
}
return proxyOptions;
}
private static async disposeBrowserSession(
session: BrowserSession | null,
): Promise<void> {
if (!session) {
return;
}
await SyntheticMonitorExecutor.safeClosePage(session.page);
await SyntheticMonitorExecutor.safeCloseBrowserContexts({
browser: session.browser,
});
await SyntheticMonitorExecutor.safeCloseBrowser(session.browser);
}
private static async safeClosePage(page?: Page | null): Promise<void> {
if (!page) {
return;
}
try {
if (!page.isClosed()) {
await page.close();
}
} catch (error: unknown) {
logger.warn(
`Failed to close Playwright page: ${(error as Error)?.message || error}`,
);
}
}
private static async safeCloseBrowserContext(
context?: BrowserContext | null,
): Promise<void> {
if (!context) {
return;
}
try {
await context.close();
} catch (error: unknown) {
logger.warn(
`Failed to close Playwright browser context: ${(error as Error)?.message || error}`,
);
}
}
private static async safeCloseBrowser(
browser?: Browser | null,
): Promise<void> {
if (!browser) {
return;
}
try {
if (browser.isConnected()) {
await browser.close();
}
} catch (error: unknown) {
logger.warn(
`Failed to close Playwright browser: ${(error as Error)?.message || error}`,
);
}
}
private static async safeCloseBrowserContexts(data: {
browser: Browser;
}): Promise<void> {
const contexts: Array<BrowserContext> = data.browser.contexts();
for (const context of contexts) {
await SyntheticMonitorExecutor.safeCloseBrowserContext(context);
}
}
private static async sleep(ms: number): Promise<void> {
return new Promise((resolve: () => void) => {
global.setTimeout(resolve, ms);
});
}
}

View File

@@ -0,0 +1,258 @@
import {
SYNTHETIC_MONITOR_ATTEMPT_PADDING_IN_MS,
SYNTHETIC_MONITOR_CHILD_GROUP_ID,
SYNTHETIC_MONITOR_CHILD_HOME_DIR,
SYNTHETIC_MONITOR_CHILD_USER_ID,
SYNTHETIC_MONITOR_RETRY_DELAY_IN_MS,
SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS,
} from "../Config";
import {
SyntheticMonitorExecutionChildMessage,
SyntheticMonitorExecutionRequest,
SyntheticMonitorExecutionResponse,
} from "../Types/SyntheticMonitorExecution";
import fs from "fs";
import logger from "Common/Server/Utils/Logger";
import path from "path";
import { fork, ForkOptions } from "child_process";
export default class SyntheticMonitorProcessRunner {
public static async execute(
request: SyntheticMonitorExecutionRequest,
): Promise<SyntheticMonitorExecutionResponse> {
const childScriptPath: string = path.resolve(
__dirname,
`ExecuteSyntheticMonitorScript${path.extname(__filename) || ".js"}`,
);
this.ensureChildHomeDirectory();
const forkOptions: ForkOptions = {
cwd: process.cwd(),
env: this.buildChildEnv(),
stdio: ["ignore", "pipe", "pipe", "ipc"],
detached: true,
};
if (
typeof process.getuid === "function" &&
process.getuid() === 0 &&
typeof process.getgid === "function"
) {
forkOptions.uid = SYNTHETIC_MONITOR_CHILD_USER_ID;
forkOptions.gid = SYNTHETIC_MONITOR_CHILD_GROUP_ID;
}
const child = fork(childScriptPath, [], forkOptions);
const timeoutInMS: number = this.getProcessTimeoutInMS(request);
child.stdout?.setEncoding("utf8");
child.stderr?.setEncoding("utf8");
child.stdout?.on("data", (chunk: string): void => {
logger.debug(`[synthetic-runner-child] ${chunk.trim()}`);
});
child.stderr?.on("data", (chunk: string): void => {
logger.warn(`[synthetic-runner-child] ${chunk.trim()}`);
});
return new Promise<SyntheticMonitorExecutionResponse>(
(
resolve: (value: SyntheticMonitorExecutionResponse) => void,
reject: (reason: Error) => void,
) => {
let settled: boolean = false;
const finish = (
callback: () => void,
options?: {
killChild?: boolean | undefined;
},
): void => {
if (settled) {
return;
}
settled = true;
global.clearTimeout(timeoutHandle);
child.removeAllListeners();
if (options?.killChild) {
this.killChildProcessGroup(child.pid);
}
callback();
};
const timeoutHandle: NodeJS.Timeout = global.setTimeout(() => {
finish(
() => {
reject(new Error("Synthetic monitor process timed out"));
},
{
killChild: true,
},
);
}, timeoutInMS);
child.once("error", (error: Error) => {
finish(
() => {
reject(error);
},
{
killChild: true,
},
);
});
child.once(
"message",
(message: SyntheticMonitorExecutionChildMessage) => {
if (message.type === "success") {
finish(() => {
resolve(message.payload);
});
return;
}
finish(
() => {
reject(
new Error(
message.error.stack
? `${message.error.message}\n${message.error.stack}`
: message.error.message,
),
);
},
{
killChild: true,
},
);
},
);
child.once(
"exit",
(code: number | null, signal: NodeJS.Signals | null) => {
if (settled) {
return;
}
finish(() => {
reject(
new Error(
`Synthetic runner child exited before responding (code: ${
code === null ? "null" : code
}, signal: ${signal || "none"})`,
),
);
});
},
);
child.send(request);
},
);
}
private static getProcessTimeoutInMS(
request: SyntheticMonitorExecutionRequest,
): number {
const browserCount: number = request.browserTypes?.length || 0;
const screenSizeCount: number = request.screenSizeTypes?.length || 0;
const combinationCount: number =
browserCount > 0 && screenSizeCount > 0
? browserCount * screenSizeCount
: 1;
const attemptCount: number = (request.retryCountOnError || 0) + 1;
const perAttemptTimeoutInMS: number =
SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS +
SYNTHETIC_MONITOR_ATTEMPT_PADDING_IN_MS;
return (
combinationCount *
(attemptCount * perAttemptTimeoutInMS +
(attemptCount - 1) * SYNTHETIC_MONITOR_RETRY_DELAY_IN_MS) +
5000
);
}
private static buildChildEnv(): NodeJS.ProcessEnv {
const env: Record<string, string | undefined> = {
HOME: SYNTHETIC_MONITOR_CHILD_HOME_DIR,
XDG_CACHE_HOME: SYNTHETIC_MONITOR_CHILD_HOME_DIR,
XDG_CONFIG_HOME: SYNTHETIC_MONITOR_CHILD_HOME_DIR,
XDG_DATA_HOME: SYNTHETIC_MONITOR_CHILD_HOME_DIR,
TMPDIR: "/tmp",
TMP: "/tmp",
TEMP: "/tmp",
PATH: process.env["PATH"] || "",
NODE_ENV: process.env["NODE_ENV"] || "production",
NODE_OPTIONS: process.env["NODE_OPTIONS"],
NODE_EXTRA_CA_CERTS: process.env["NODE_EXTRA_CA_CERTS"],
SSL_CERT_FILE: process.env["SSL_CERT_FILE"],
SSL_CERT_DIR: process.env["SSL_CERT_DIR"],
PLAYWRIGHT_BROWSERS_PATH:
process.env["PLAYWRIGHT_BROWSERS_PATH"] || "/ms-playwright-browsers",
HTTP_PROXY_URL: process.env["HTTP_PROXY_URL"],
HTTPS_PROXY_URL: process.env["HTTPS_PROXY_URL"],
NO_PROXY: process.env["NO_PROXY"],
http_proxy: process.env["http_proxy"],
https_proxy: process.env["https_proxy"],
no_proxy: process.env["no_proxy"],
TZ: process.env["TZ"],
LANG: process.env["LANG"],
LANGUAGE: process.env["LANGUAGE"],
LC_ALL: process.env["LC_ALL"],
};
return Object.fromEntries(
Object.entries(env).filter(
(entry: [string, string | undefined]): entry is [string, string] => {
return typeof entry[1] === "string";
},
),
);
}
private static ensureChildHomeDirectory(): void {
if (!fs.existsSync(SYNTHETIC_MONITOR_CHILD_HOME_DIR)) {
fs.mkdirSync(SYNTHETIC_MONITOR_CHILD_HOME_DIR, {
recursive: true,
mode: 0o755,
});
}
if (
typeof process.getuid === "function" &&
process.getuid() === 0 &&
typeof process.getgid === "function"
) {
fs.chownSync(
SYNTHETIC_MONITOR_CHILD_HOME_DIR,
SYNTHETIC_MONITOR_CHILD_USER_ID,
SYNTHETIC_MONITOR_CHILD_GROUP_ID,
);
}
}
private static killChildProcessGroup(pid?: number): void {
if (!pid) {
return;
}
try {
process.kill(-pid, "SIGKILL");
} catch {
try {
process.kill(pid, "SIGKILL");
} catch {
return;
}
}
}
}

View File

@@ -0,0 +1,47 @@
import { PORT, SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS } from "./Config";
import SyntheticMonitorAPI from "./API/SyntheticMonitor";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import logger from "Common/Server/Utils/Logger";
import App from "Common/Server/Utils/StartServer";
import Telemetry from "Common/Server/Utils/Telemetry";
import Express, { ExpressApplication } from "Common/Server/Utils/Express";
import "ejs";
const APP_NAME: string = "synthetic-runner";
const init: PromiseVoidFunction = async (): Promise<void> => {
try {
Telemetry.init({
serviceName: APP_NAME,
});
logger.info(
`Synthetic Runner Service - Script timeout: ${SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS}ms`,
);
await App.init({
appName: APP_NAME,
port: PORT,
isFrontendApp: false,
statusOptions: {
liveCheck: async () => {},
readyCheck: async () => {},
},
});
const app: ExpressApplication = Express.getExpressApp();
app.use("/synthetic-monitor", SyntheticMonitorAPI);
await App.addDefaultRoutes();
} catch (err: unknown) {
logger.error("Synthetic runner init failed:");
logger.error(err);
throw err;
}
};
init().catch((err: Error) => {
logger.error(err);
logger.error("Exiting node process");
process.exit(1);
});

View File

@@ -0,0 +1,32 @@
import BrowserType from "Common/Types/Monitor/SyntheticMonitors/BrowserType";
import ScreenSizeType from "Common/Types/Monitor/SyntheticMonitors/ScreenSizeType";
import SyntheticMonitorResponse from "Common/Types/Monitor/SyntheticMonitors/SyntheticMonitorResponse";
export interface SyntheticMonitorExecutionRequest {
monitorId?: string | undefined;
screenSizeTypes?: Array<ScreenSizeType> | undefined;
browserTypes?: Array<BrowserType> | undefined;
script: string;
retryCountOnError?: number | undefined;
}
export interface SyntheticMonitorExecutionResponse {
results: Array<SyntheticMonitorResponse>;
}
export interface SyntheticMonitorExecutionChildSuccessMessage {
type: "success";
payload: SyntheticMonitorExecutionResponse;
}
export interface SyntheticMonitorExecutionChildErrorMessage {
type: "error";
error: {
message: string;
stack?: string | undefined;
};
}
export type SyntheticMonitorExecutionChildMessage =
| SyntheticMonitorExecutionChildSuccessMessage
| SyntheticMonitorExecutionChildErrorMessage;

View File

@@ -1,16 +1,20 @@
import { PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS } from "../../../Config";
import ProxyConfig from "../../ProxyConfig";
import BadDataException from "Common/Types/Exception/BadDataException";
import ReturnResult from "Common/Types/IsolatedVM/ReturnResult";
import {
PROBE_SYNTHETIC_RUNNER_URL,
PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS,
} from "../../../Config";
import ClusterKeyAuthorization from "Common/Server/Middleware/ClusterKeyAuthorization";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import HTTPMethod from "Common/Types/API/HTTPMethod";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import BrowserType from "Common/Types/Monitor/SyntheticMonitors/BrowserType";
import ScreenSizeType from "Common/Types/Monitor/SyntheticMonitors/ScreenSizeType";
import SyntheticMonitorResponse from "Common/Types/Monitor/SyntheticMonitors/SyntheticMonitorResponse";
import ObjectID from "Common/Types/ObjectID";
import logger from "Common/Server/Utils/Logger";
import VMRunner from "Common/Server/Utils/VM/VMRunner";
import { Browser, BrowserContext, Page, chromium, firefox } from "playwright";
import LocalFile from "Common/Server/Utils/LocalFile";
import os from "os";
import URL from "Common/Types/API/URL";
import { JSONObject } from "Common/Types/JSON";
import API from "Common/Utils/API";
import { SyntheticMonitorExecutionRequest } from "../../../SyntheticRunner/Types/SyntheticMonitorExecution";
export interface SyntheticMonitorOptions {
monitorId?: ObjectID | undefined;
@@ -20,503 +24,101 @@ export interface SyntheticMonitorOptions {
retryCountOnError?: number | undefined;
}
interface BrowserLaunchOptions {
executablePath?: string;
proxy?: {
server: string;
username?: string;
password?: string;
bypass?: string;
};
args?: string[];
headless?: boolean;
devtools?: boolean;
timeout?: number;
}
interface BrowserSession {
browser: Browser;
context: BrowserContext;
page: Page;
}
export default class SyntheticMonitor {
public static async execute(
options: SyntheticMonitorOptions,
): Promise<Array<SyntheticMonitorResponse> | null> {
const request: SyntheticMonitorExecutionRequest = {
monitorId: options.monitorId?.toString(),
screenSizeTypes: options.screenSizeTypes,
browserTypes: options.browserTypes,
script: options.script,
retryCountOnError: options.retryCountOnError,
};
try {
const result: HTTPResponse<JSONObject> | HTTPErrorResponse =
await API.fetch<JSONObject>({
method: HTTPMethod.POST,
url: URL.fromString(PROBE_SYNTHETIC_RUNNER_URL.toString()).addRoute(
"/synthetic-monitor/run",
),
data: request as unknown as JSONObject,
headers: ClusterKeyAuthorization.getClusterKeyHeaders(),
options: {
timeout: this.getRequestTimeoutInMS(options),
},
});
if (result instanceof HTTPErrorResponse || result.isFailure()) {
const message: string =
result instanceof HTTPErrorResponse
? result.message || "Synthetic runner request failed"
: `Synthetic runner request failed with status code ${result.statusCode}`;
logger.error(message);
return this.buildFailureResults(options, message);
}
const rawResults: unknown = result.data["results"];
if (!Array.isArray(rawResults)) {
const message: string = "Synthetic runner returned an invalid payload";
logger.error(message);
return this.buildFailureResults(options, message);
}
return rawResults as Array<SyntheticMonitorResponse>;
} catch (err: unknown) {
logger.error(err);
const message: string =
(err as Error)?.message || (err as Error)?.toString() || String(err);
return this.buildFailureResults(options, message);
}
}
private static buildFailureResults(
options: SyntheticMonitorOptions,
message: string,
): Array<SyntheticMonitorResponse> {
const results: Array<SyntheticMonitorResponse> = [];
for (const browserType of options.browserTypes || []) {
for (const screenSizeType of options.screenSizeTypes || []) {
logger.debug(
`Running Synthetic Monitor: ${options?.monitorId?.toString()}, Screen Size: ${screenSizeType}, Browser: ${browserType}`,
);
const result: SyntheticMonitorResponse | null =
await this.executeWithRetry({
script: options.script,
browserType: browserType,
screenSizeType: screenSizeType,
retryCountOnError: options.retryCountOnError || 0,
});
if (result) {
result.browserType = browserType;
result.screenSizeType = screenSizeType;
results.push(result);
}
results.push({
logMessages: [],
scriptError: message,
result: undefined,
screenshots: {},
executionTimeInMS: 0,
browserType: browserType,
screenSizeType: screenSizeType,
});
}
}
return results;
}
private static async executeWithRetry(options: {
script: string;
browserType: BrowserType;
screenSizeType: ScreenSizeType;
retryCountOnError: number;
currentRetry?: number;
}): Promise<SyntheticMonitorResponse | null> {
const currentRetry: number = options.currentRetry || 0;
const maxRetries: number = options.retryCountOnError;
private static getRequestTimeoutInMS(
options: SyntheticMonitorOptions,
): number {
const browserCount: number = options.browserTypes?.length || 0;
const screenSizeCount: number = options.screenSizeTypes?.length || 0;
const combinationCount: number =
browserCount > 0 && screenSizeCount > 0
? browserCount * screenSizeCount
: 1;
const attemptCount: number = (options.retryCountOnError || 0) + 1;
const result: SyntheticMonitorResponse | null =
await this.executeByBrowserAndScreenSize({
script: options.script,
browserType: options.browserType,
screenSizeType: options.screenSizeType,
});
// If there's an error and we haven't exceeded retry count, retry
if (result?.scriptError && currentRetry < maxRetries) {
logger.debug(
`Synthetic Monitor script error, retrying (${currentRetry + 1}/${maxRetries}): ${result.scriptError}`,
);
// Wait a bit before retrying
await new Promise((resolve: (value: void) => void) => {
setTimeout(resolve, 1000);
});
return this.executeWithRetry({
script: options.script,
browserType: options.browserType,
screenSizeType: options.screenSizeType,
retryCountOnError: maxRetries,
currentRetry: currentRetry + 1,
});
}
return result;
}
private static async executeByBrowserAndScreenSize(options: {
script: string;
browserType: BrowserType;
screenSizeType: ScreenSizeType;
}): Promise<SyntheticMonitorResponse | null> {
if (!options) {
// this should never happen
options = {
script: "",
browserType: BrowserType.Chromium,
screenSizeType: ScreenSizeType.Desktop,
};
}
const scriptResult: SyntheticMonitorResponse = {
logMessages: [],
scriptError: undefined,
result: undefined,
screenshots: {},
executionTimeInMS: 0,
browserType: options.browserType,
screenSizeType: options.screenSizeType,
};
let browserSession: BrowserSession | null = null;
try {
let result: ReturnResult | null = null;
const startTime: [number, number] = process.hrtime();
browserSession = await SyntheticMonitor.getPageByBrowserType({
browserType: options.browserType,
screenSizeType: options.screenSizeType,
});
if (!browserSession) {
throw new BadDataException(
"Could not create Playwright browser session",
);
}
/*
* Only expose `page` to the sandbox — never the `browser` object.
* Exposing `browser` allows RCE via browser.browserType().launch({executablePath:"/bin/sh"}).
*/
result = await VMRunner.runCodeInNodeVM({
code: options.script,
options: {
timeout: PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS,
args: {},
context: {
page: browserSession.page,
screenSizeType: options.screenSizeType,
browserType: options.browserType,
},
},
});
const endTime: [number, number] = process.hrtime(startTime);
const executionTimeInMS: number = Math.ceil(
(endTime[0] * 1000000000 + endTime[1]) / 1000000,
);
scriptResult.executionTimeInMS = executionTimeInMS;
scriptResult.logMessages = result.logMessages;
if (result.returnValue?.screenshots) {
if (!scriptResult.screenshots) {
scriptResult.screenshots = {};
}
for (const screenshotName in result.returnValue.screenshots) {
if (!result.returnValue.screenshots[screenshotName]) {
continue;
}
// check if this is of type Buffer. If it is not, continue.
if (
!(result.returnValue.screenshots[screenshotName] instanceof Buffer)
) {
continue;
}
const screenshotBuffer: Buffer = result.returnValue.screenshots[
screenshotName
] as Buffer;
scriptResult.screenshots[screenshotName] =
screenshotBuffer.toString("base64"); // convert screenshots to base 64
}
}
scriptResult.result = result?.returnValue?.data;
} catch (err: unknown) {
logger.error(err);
scriptResult.scriptError =
(err as Error)?.message || (err as Error).toString();
} finally {
// Always dispose browser session to prevent zombie processes
await SyntheticMonitor.disposeBrowserSession(browserSession);
}
return scriptResult;
}
private static getViewportHeightAndWidth(options: {
screenSizeType: ScreenSizeType;
}): {
height: number;
width: number;
} {
let viewPortHeight: number = 0;
let viewPortWidth: number = 0;
switch (options.screenSizeType) {
case ScreenSizeType.Desktop:
viewPortHeight = 1080;
viewPortWidth = 1920;
break;
case ScreenSizeType.Mobile:
viewPortHeight = 640;
viewPortWidth = 360;
break;
case ScreenSizeType.Tablet:
viewPortHeight = 768;
viewPortWidth = 1024;
break;
default:
viewPortHeight = 1080;
viewPortWidth = 1920;
break;
}
return { height: viewPortHeight, width: viewPortWidth };
}
private static getPlaywrightBrowsersPath(): string {
return (
process.env["PLAYWRIGHT_BROWSERS_PATH"] ||
`${os.homedir()}/.cache/ms-playwright`
combinationCount *
(attemptCount * (PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS + 30000) +
(attemptCount - 1) * 1000) +
5000
);
}
public static async getChromeExecutablePath(): Promise<string> {
const browsersPath: string = this.getPlaywrightBrowsersPath();
const doesDirectoryExist: boolean =
await LocalFile.doesDirectoryExist(browsersPath);
if (!doesDirectoryExist) {
throw new BadDataException("Chrome executable path not found.");
}
// get list of files in the directory
const directories: string[] =
await LocalFile.getListOfDirectories(browsersPath);
if (directories.length === 0) {
throw new BadDataException("Chrome executable path not found.");
}
const chromeInstallationName: string | undefined = directories.find(
(directory: string) => {
return directory.includes("chromium");
},
);
if (!chromeInstallationName) {
throw new BadDataException("Chrome executable path not found.");
}
const chromeExecutableCandidates: Array<string> = [
`${browsersPath}/${chromeInstallationName}/chrome-linux/chrome`,
`${browsersPath}/${chromeInstallationName}/chrome-linux64/chrome`,
`${browsersPath}/${chromeInstallationName}/chrome64/chrome`,
`${browsersPath}/${chromeInstallationName}/chrome/chrome`,
];
for (const executablePath of chromeExecutableCandidates) {
if (await LocalFile.doesFileExist(executablePath)) {
return executablePath;
}
}
throw new BadDataException("Chrome executable path not found.");
}
public static async getFirefoxExecutablePath(): Promise<string> {
const browsersPath: string = this.getPlaywrightBrowsersPath();
const doesDirectoryExist: boolean =
await LocalFile.doesDirectoryExist(browsersPath);
if (!doesDirectoryExist) {
throw new BadDataException("Firefox executable path not found.");
}
// get list of files in the directory
const directories: string[] =
await LocalFile.getListOfDirectories(browsersPath);
if (directories.length === 0) {
throw new BadDataException("Firefox executable path not found.");
}
const firefoxInstallationName: string | undefined = directories.find(
(directory: string) => {
return directory.includes("firefox");
},
);
if (!firefoxInstallationName) {
throw new BadDataException("Firefox executable path not found.");
}
const firefoxExecutableCandidates: Array<string> = [
`${browsersPath}/${firefoxInstallationName}/firefox/firefox`,
`${browsersPath}/${firefoxInstallationName}/firefox-linux64/firefox`,
`${browsersPath}/${firefoxInstallationName}/firefox64/firefox`,
`${browsersPath}/${firefoxInstallationName}/firefox-64/firefox`,
];
for (const executablePath of firefoxExecutableCandidates) {
if (await LocalFile.doesFileExist(executablePath)) {
return executablePath;
}
}
throw new BadDataException("Firefox executable path not found.");
}
private static async getPageByBrowserType(data: {
browserType: BrowserType;
screenSizeType: ScreenSizeType;
}): Promise<BrowserSession> {
const viewport: {
height: number;
width: number;
} = SyntheticMonitor.getViewportHeightAndWidth({
screenSizeType: data.screenSizeType,
});
// Prepare browser launch options with proxy support
const baseOptions: BrowserLaunchOptions = {};
// Configure proxy if available
if (ProxyConfig.isProxyConfigured()) {
const httpsProxyUrl: string | null = ProxyConfig.getHttpsProxyUrl();
const httpProxyUrl: string | null = ProxyConfig.getHttpProxyUrl();
// Prefer HTTPS proxy, fall back to HTTP proxy
const proxyUrl: string | null = httpsProxyUrl || httpProxyUrl;
if (proxyUrl) {
baseOptions.proxy = {
server: proxyUrl,
};
// Extract username and password if present in proxy URL
try {
const parsedUrl: globalThis.URL = new URL(proxyUrl);
if (parsedUrl.username && parsedUrl.password) {
baseOptions.proxy.username = parsedUrl.username;
baseOptions.proxy.password = parsedUrl.password;
}
} catch (error) {
logger.warn(`Failed to parse proxy URL for authentication: ${error}`);
}
logger.debug(
`Synthetic Monitor using proxy: ${proxyUrl} (HTTPS: ${Boolean(httpsProxyUrl)}, HTTP: ${Boolean(httpProxyUrl)})`,
);
}
}
if (data.browserType === BrowserType.Chromium) {
const browser: Browser = await chromium.launch({
executablePath: await this.getChromeExecutablePath(),
...baseOptions,
});
const context: BrowserContext = await browser.newContext({
viewport: {
width: viewport.width,
height: viewport.height,
},
});
const page: Page = await context.newPage();
return {
browser,
context,
page,
};
}
if (data.browserType === BrowserType.Firefox) {
const browser: Browser = await firefox.launch({
executablePath: await this.getFirefoxExecutablePath(),
...baseOptions,
});
let context: BrowserContext | null = null;
try {
context = await browser.newContext({
viewport: {
width: viewport.width,
height: viewport.height,
},
});
const page: Page = await context.newPage();
return {
browser,
context,
page,
};
} catch (error) {
await SyntheticMonitor.safeCloseBrowserContext(context);
await SyntheticMonitor.safeCloseBrowser(browser);
throw error;
}
}
throw new BadDataException("Invalid Browser Type.");
}
private static async disposeBrowserSession(
session: BrowserSession | null,
): Promise<void> {
if (!session) {
return;
}
await SyntheticMonitor.safeClosePage(session.page);
await SyntheticMonitor.safeCloseBrowserContexts({
browser: session.browser,
});
await SyntheticMonitor.safeCloseBrowser(session.browser);
}
private static async safeClosePage(page?: Page | null): Promise<void> {
if (!page) {
return;
}
try {
if (!page.isClosed()) {
await page.close();
}
} catch (error) {
logger.warn(
`Failed to close Playwright page: ${(error as Error)?.message || error}`,
);
}
}
private static async safeCloseBrowserContext(
context?: BrowserContext | null,
): Promise<void> {
if (!context) {
return;
}
try {
await context.close();
} catch (error) {
logger.warn(
`Failed to close Playwright browser context: ${(error as Error)?.message || error}`,
);
}
}
private static async safeCloseBrowser(
browser?: Browser | null,
): Promise<void> {
if (!browser) {
return;
}
try {
if (browser.isConnected()) {
await browser.close();
}
} catch (error) {
logger.warn(
`Failed to close Playwright browser: ${(error as Error)?.message || error}`,
);
}
}
private static async safeCloseBrowserContexts(data: {
browser: Browser;
}): Promise<void> {
if (!data.browser || !data.browser.contexts) {
return;
}
const contexts: Array<BrowserContext> = data.browser.contexts();
for (const context of contexts) {
await SyntheticMonitor.safeCloseBrowserContext(context);
}
}
}

View File

@@ -9,6 +9,7 @@
"main": "index.js",
"scripts": {
"start": "export NODE_OPTIONS='--max-old-space-size=8096' && node --require ts-node/register Index.ts",
"start:synthetic-runner": "export NODE_OPTIONS='--max-old-space-size=8096' && node --require ts-node/register SyntheticRunner/Index.ts",
"compile": "tsc",
"clear-modules": "rm -rf node_modules && rm package-lock.json && npm install",
"dev": "npx nodemon",

View File

@@ -1,410 +1,450 @@
x-common-variables: &common-variables
HOST: ${HOST}
PROVISION_SSL: ${PROVISION_SSL}
HOST: ${HOST}
PROVISION_SSL: ${PROVISION_SSL}
HTTP_PROTOCOL: ${HTTP_PROTOCOL}
HTTP_PROTOCOL: ${HTTP_PROTOCOL}
STATUS_PAGE_CNAME_RECORD: ${STATUS_PAGE_CNAME_RECORD}
STATUS_PAGE_CNAME_RECORD: ${STATUS_PAGE_CNAME_RECORD}
LOG_LEVEL: ${LOG_LEVEL}
LOG_LEVEL: ${LOG_LEVEL}
NODE_ENV: ${ENVIRONMENT}
BILLING_ENABLED: ${BILLING_ENABLED}
IS_ENTERPRISE_EDITION: ${IS_ENTERPRISE_EDITION}
BILLING_PUBLIC_KEY: ${BILLING_PUBLIC_KEY}
SUBSCRIPTION_PLAN_BASIC: ${SUBSCRIPTION_PLAN_BASIC}
SUBSCRIPTION_PLAN_GROWTH: ${SUBSCRIPTION_PLAN_GROWTH}
SUBSCRIPTION_PLAN_SCALE: ${SUBSCRIPTION_PLAN_SCALE}
SUBSCRIPTION_PLAN_ENTERPRISE: ${SUBSCRIPTION_PLAN_ENTERPRISE}
ANALYTICS_KEY: ${ANALYTICS_KEY}
ANALYTICS_HOST: ${ANALYTICS_HOST}
CAPTCHA_ENABLED: ${CAPTCHA_ENABLED}
CAPTCHA_SITE_KEY: ${CAPTCHA_SITE_KEY}
NODE_ENV: ${ENVIRONMENT}
BILLING_ENABLED: ${BILLING_ENABLED}
IS_ENTERPRISE_EDITION: ${IS_ENTERPRISE_EDITION}
BILLING_PUBLIC_KEY: ${BILLING_PUBLIC_KEY}
SUBSCRIPTION_PLAN_BASIC: ${SUBSCRIPTION_PLAN_BASIC}
SUBSCRIPTION_PLAN_GROWTH: ${SUBSCRIPTION_PLAN_GROWTH}
SUBSCRIPTION_PLAN_SCALE: ${SUBSCRIPTION_PLAN_SCALE}
SUBSCRIPTION_PLAN_ENTERPRISE: ${SUBSCRIPTION_PLAN_ENTERPRISE}
ANALYTICS_KEY: ${ANALYTICS_KEY}
ANALYTICS_HOST: ${ANALYTICS_HOST}
CAPTCHA_ENABLED: ${CAPTCHA_ENABLED}
CAPTCHA_SITE_KEY: ${CAPTCHA_SITE_KEY}
# VAPID keys for Web Push Notifications
VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY}
VAPID_SUBJECT: ${VAPID_SUBJECT}
# VAPID keys for Web Push Notifications
VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY}
VAPID_SUBJECT: ${VAPID_SUBJECT}
ALLOWED_ACTIVE_MONITOR_COUNT_IN_FREE_PLAN: ${ALLOWED_ACTIVE_MONITOR_COUNT_IN_FREE_PLAN}
ALLOWED_ACTIVE_MONITOR_COUNT_IN_FREE_PLAN: ${ALLOWED_ACTIVE_MONITOR_COUNT_IN_FREE_PLAN}
SERVER_APP_HOSTNAME: app
SERVER_TELEMETRY_HOSTNAME: telemetry
SERVER_WORKER_HOSTNAME: worker
SERVER_HOME_HOSTNAME: home
#Ports. Usually they don't need to change.
APP_PORT: ${APP_PORT}
HOME_PORT: ${HOME_PORT}
TELEMETRY_PORT: ${TELEMETRY_PORT}
WORKER_PORT: ${WORKER_PORT}
OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT: ${OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT}
OPENTELEMETRY_EXPORTER_OTLP_HEADERS: ${OPENTELEMETRY_EXPORTER_OTLP_HEADERS}
SERVER_APP_HOSTNAME: app
SERVER_TELEMETRY_HOSTNAME: telemetry
SERVER_WORKER_HOSTNAME: worker
SERVER_HOME_HOSTNAME: home
#Ports. Usually they don't need to change.
APP_PORT: ${APP_PORT}
HOME_PORT: ${HOME_PORT}
TELEMETRY_PORT: ${TELEMETRY_PORT}
WORKER_PORT: ${WORKER_PORT}
OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT: ${OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT}
OPENTELEMETRY_EXPORTER_OTLP_HEADERS: ${OPENTELEMETRY_EXPORTER_OTLP_HEADERS}
SLACK_APP_CLIENT_ID: ${SLACK_APP_CLIENT_ID}
SLACK_APP_CLIENT_ID: ${SLACK_APP_CLIENT_ID}
MICROSOFT_TEAMS_APP_CLIENT_ID: ${MICROSOFT_TEAMS_APP_CLIENT_ID}
MICROSOFT_TEAMS_APP_TENANT_ID: ${MICROSOFT_TEAMS_APP_TENANT_ID}
MICROSOFT_TEAMS_APP_CLIENT_ID: ${MICROSOFT_TEAMS_APP_CLIENT_ID}
MICROSOFT_TEAMS_APP_TENANT_ID: ${MICROSOFT_TEAMS_APP_TENANT_ID}
x-common-runtime-variables: &common-runtime-variables
<<: *common-variables
ONEUPTIME_SECRET: ${ONEUPTIME_SECRET}
<<: *common-variables
ONEUPTIME_SECRET: ${ONEUPTIME_SECRET}
VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY}
VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY}
EXPO_ACCESS_TOKEN: ${EXPO_ACCESS_TOKEN}
PUSH_NOTIFICATION_RELAY_URL: ${PUSH_NOTIFICATION_RELAY_URL}
EXPO_ACCESS_TOKEN: ${EXPO_ACCESS_TOKEN}
PUSH_NOTIFICATION_RELAY_URL: ${PUSH_NOTIFICATION_RELAY_URL}
DATABASE_PORT: ${DATABASE_PORT}
DATABASE_USERNAME: ${DATABASE_USERNAME}
DATABASE_PASSWORD: ${DATABASE_PASSWORD}
DATABASE_NAME: ${DATABASE_NAME}
DATABASE_HOST: ${DATABASE_HOST}
DATABASE_SSL_CA: ${DATABASE_SSL_CA}
DATABASE_SSL_KEY: ${DATABASE_SSL_KEY}
DATABASE_SSL_CERT: ${DATABASE_SSL_CERT}
DATABASE_SSL_REJECT_UNAUTHORIZED: ${DATABASE_SSL_REJECT_UNAUTHORIZED}
LETS_ENCRYPT_NOTIFICATION_EMAIL: ${LETS_ENCRYPT_NOTIFICATION_EMAIL}
LETS_ENCRYPT_ACCOUNT_KEY: ${LETS_ENCRYPT_ACCOUNT_KEY}
DATABASE_PORT: ${DATABASE_PORT}
DATABASE_USERNAME: ${DATABASE_USERNAME}
DATABASE_PASSWORD: ${DATABASE_PASSWORD}
DATABASE_NAME: ${DATABASE_NAME}
DATABASE_HOST: ${DATABASE_HOST}
DATABASE_SSL_CA: ${DATABASE_SSL_CA}
DATABASE_SSL_KEY: ${DATABASE_SSL_KEY}
DATABASE_SSL_CERT: ${DATABASE_SSL_CERT}
DATABASE_SSL_REJECT_UNAUTHORIZED: ${DATABASE_SSL_REJECT_UNAUTHORIZED}
LETS_ENCRYPT_NOTIFICATION_EMAIL: ${LETS_ENCRYPT_NOTIFICATION_EMAIL}
LETS_ENCRYPT_ACCOUNT_KEY: ${LETS_ENCRYPT_ACCOUNT_KEY}
REDIS_USERNAME: ${REDIS_USERNAME}
REDIS_PASSWORD: ${REDIS_PASSWORD}
REDIS_HOST: ${REDIS_HOST}
REDIS_PORT: ${REDIS_PORT}
REDIS_DB: ${REDIS_DB}
REDIS_IP_FAMILY: ${REDIS_IP_FAMILY}
REDIS_TLS_CA: ${REDIS_TLS_CA}
REDIS_TLS_SENTINEL_MODE: ${REDIS_TLS_SENTINEL_MODE}
REDIS_USERNAME: ${REDIS_USERNAME}
REDIS_PASSWORD: ${REDIS_PASSWORD}
REDIS_HOST: ${REDIS_HOST}
REDIS_PORT: ${REDIS_PORT}
REDIS_DB: ${REDIS_DB}
REDIS_IP_FAMILY: ${REDIS_IP_FAMILY}
REDIS_TLS_CA: ${REDIS_TLS_CA}
REDIS_TLS_SENTINEL_MODE: ${REDIS_TLS_SENTINEL_MODE}
ENCRYPTION_SECRET: ${ENCRYPTION_SECRET}
ENCRYPTION_SECRET: ${ENCRYPTION_SECRET}
BILLING_PRIVATE_KEY: ${BILLING_PRIVATE_KEY}
BILLING_PUBLIC_KEY: ${BILLING_PUBLIC_KEY}
BILLING_ENABLED: ${BILLING_ENABLED}
BILLING_WEBHOOK_SECRET: ${BILLING_WEBHOOK_SECRET}
BILLING_PRIVATE_KEY: ${BILLING_PRIVATE_KEY}
BILLING_PUBLIC_KEY: ${BILLING_PUBLIC_KEY}
BILLING_ENABLED: ${BILLING_ENABLED}
BILLING_WEBHOOK_SECRET: ${BILLING_WEBHOOK_SECRET}
CLICKHOUSE_USER: ${CLICKHOUSE_USER}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
CLICKHOUSE_DATABASE: ${CLICKHOUSE_DATABASE}
CLICKHOUSE_HOST: ${CLICKHOUSE_HOST}
CLICKHOUSE_PORT: ${CLICKHOUSE_PORT}
CLICKHOUSE_USER: ${CLICKHOUSE_USER}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
CLICKHOUSE_DATABASE: ${CLICKHOUSE_DATABASE}
CLICKHOUSE_HOST: ${CLICKHOUSE_HOST}
CLICKHOUSE_PORT: ${CLICKHOUSE_PORT}
AVERAGE_SPAN_ROW_SIZE_IN_BYTES: ${AVERAGE_SPAN_ROW_SIZE_IN_BYTES}
AVERAGE_LOG_ROW_SIZE_IN_BYTES: ${AVERAGE_LOG_ROW_SIZE_IN_BYTES}
AVERAGE_METRIC_ROW_SIZE_IN_BYTES: ${AVERAGE_METRIC_ROW_SIZE_IN_BYTES}
AVERAGE_EXCEPTION_ROW_SIZE_IN_BYTES: ${AVERAGE_EXCEPTION_ROW_SIZE_IN_BYTES}
AVERAGE_SPAN_ROW_SIZE_IN_BYTES: ${AVERAGE_SPAN_ROW_SIZE_IN_BYTES}
AVERAGE_LOG_ROW_SIZE_IN_BYTES: ${AVERAGE_LOG_ROW_SIZE_IN_BYTES}
AVERAGE_METRIC_ROW_SIZE_IN_BYTES: ${AVERAGE_METRIC_ROW_SIZE_IN_BYTES}
AVERAGE_EXCEPTION_ROW_SIZE_IN_BYTES: ${AVERAGE_EXCEPTION_ROW_SIZE_IN_BYTES}
WORKFLOW_SCRIPT_TIMEOUT_IN_MS: ${WORKFLOW_SCRIPT_TIMEOUT_IN_MS}
WORKFLOW_TIMEOUT_IN_MS: ${WORKFLOW_TIMEOUT_IN_MS}
WORKFLOW_SCRIPT_TIMEOUT_IN_MS: ${WORKFLOW_SCRIPT_TIMEOUT_IN_MS}
WORKFLOW_TIMEOUT_IN_MS: ${WORKFLOW_TIMEOUT_IN_MS}
DISABLE_AUTOMATIC_INCIDENT_CREATION: ${DISABLE_AUTOMATIC_INCIDENT_CREATION}
DISABLE_AUTOMATIC_INCIDENT_CREATION: ${DISABLE_AUTOMATIC_INCIDENT_CREATION}
DISABLE_AUTOMATIC_ALERT_CREATION: ${DISABLE_AUTOMATIC_ALERT_CREATION}
DISABLE_AUTOMATIC_ALERT_CREATION: ${DISABLE_AUTOMATIC_ALERT_CREATION}
# Open Source Deployment Webhook
OPEN_SOURCE_DEPLOYMENT_WEBHOOK_URL: ${OPEN_SOURCE_DEPLOYMENT_WEBHOOK_URL}
# Open Source Deployment Webhook
OPEN_SOURCE_DEPLOYMENT_WEBHOOK_URL: ${OPEN_SOURCE_DEPLOYMENT_WEBHOOK_URL}
# Notification Webhooks
NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_USER: ${NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_USER}
NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_PROJECT: ${NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_PROJECT}
NOTIFICATION_SLACK_WEBHOOK_ON_DELETED_PROJECT: ${NOTIFICATION_SLACK_WEBHOOK_ON_DELETED_PROJECT}
NOTIFICATION_SLACK_WEBHOOK_ON_SUBSCRIPTION_UPDATE: ${NOTIFICATION_SLACK_WEBHOOK_ON_SUBSCRIPTION_UPDATE}
# Notification Webhooks
NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_USER: ${NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_USER}
NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_PROJECT: ${NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_PROJECT}
NOTIFICATION_SLACK_WEBHOOK_ON_DELETED_PROJECT: ${NOTIFICATION_SLACK_WEBHOOK_ON_DELETED_PROJECT}
NOTIFICATION_SLACK_WEBHOOK_ON_SUBSCRIPTION_UPDATE: ${NOTIFICATION_SLACK_WEBHOOK_ON_SUBSCRIPTION_UPDATE}
SLACK_APP_CLIENT_SECRET: ${SLACK_APP_CLIENT_SECRET}
SLACK_APP_SIGNING_SECRET: ${SLACK_APP_SIGNING_SECRET}
SLACK_APP_CLIENT_SECRET: ${SLACK_APP_CLIENT_SECRET}
SLACK_APP_SIGNING_SECRET: ${SLACK_APP_SIGNING_SECRET}
# Microsoft Teams Configuration
# Microsoft Teams Configuration
MICROSOFT_TEAMS_APP_CLIENT_SECRET: ${MICROSOFT_TEAMS_APP_CLIENT_SECRET}
CAPTCHA_SECRET_KEY: ${CAPTCHA_SECRET_KEY}
MICROSOFT_TEAMS_APP_CLIENT_SECRET: ${MICROSOFT_TEAMS_APP_CLIENT_SECRET}
CAPTCHA_SECRET_KEY: ${CAPTCHA_SECRET_KEY}
# GitHub App Configuration
GITHUB_APP_ID: ${GITHUB_APP_ID}
GITHUB_APP_NAME: ${GITHUB_APP_NAME}
GITHUB_APP_CLIENT_ID: ${GITHUB_APP_CLIENT_ID}
GITHUB_APP_CLIENT_SECRET: ${GITHUB_APP_CLIENT_SECRET}
GITHUB_APP_PRIVATE_KEY: ${GITHUB_APP_PRIVATE_KEY}
GITHUB_APP_WEBHOOK_SECRET: ${GITHUB_APP_WEBHOOK_SECRET}
# GitHub App Configuration
GITHUB_APP_ID: ${GITHUB_APP_ID}
GITHUB_APP_NAME: ${GITHUB_APP_NAME}
GITHUB_APP_CLIENT_ID: ${GITHUB_APP_CLIENT_ID}
GITHUB_APP_CLIENT_SECRET: ${GITHUB_APP_CLIENT_SECRET}
GITHUB_APP_PRIVATE_KEY: ${GITHUB_APP_PRIVATE_KEY}
GITHUB_APP_WEBHOOK_SECRET: ${GITHUB_APP_WEBHOOK_SECRET}
# Inbound Email Configuration (for Incoming Email Monitor)
INBOUND_EMAIL_PROVIDER: ${INBOUND_EMAIL_PROVIDER}
INBOUND_EMAIL_DOMAIN: ${INBOUND_EMAIL_DOMAIN}
INBOUND_EMAIL_WEBHOOK_SECRET: ${INBOUND_EMAIL_WEBHOOK_SECRET}
# Inbound Email Configuration (for Incoming Email Monitor)
INBOUND_EMAIL_PROVIDER: ${INBOUND_EMAIL_PROVIDER}
INBOUND_EMAIL_DOMAIN: ${INBOUND_EMAIL_DOMAIN}
INBOUND_EMAIL_WEBHOOK_SECRET: ${INBOUND_EMAIL_WEBHOOK_SECRET}
services:
redis:
image: redis:7.0.12
restart: always
networks:
- oneuptime
command: redis-server --requirepass "${REDIS_PASSWORD}" --save "" --appendonly no
environment:
REDIS_PASSWORD: ${REDIS_PASSWORD}
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
logging:
driver: "local"
options:
max-size: "1000m"
redis:
image: redis:7.0.12
restart: always
networks:
- oneuptime
command: redis-server --requirepass "${REDIS_PASSWORD}" --save "" --appendonly no
environment:
REDIS_PASSWORD: ${REDIS_PASSWORD}
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
logging:
driver: "local"
options:
max-size: "1000m"
clickhouse:
image: clickhouse/clickhouse-server:25.7
restart: always
environment:
CLICKHOUSE_USER: ${CLICKHOUSE_USER}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
CLICKHOUSE_DB: ${CLICKHOUSE_DATABASE}
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
networks:
- oneuptime
volumes:
- clickhouse:/var/lib/clickhouse/
healthcheck:
test: ["CMD-SHELL", "clickhouse-client --query 'SELECT 1'"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
logging:
driver: "local"
options:
max-size: "1000m"
clickhouse:
image: clickhouse/clickhouse-server:25.7
restart: always
environment:
CLICKHOUSE_USER: ${CLICKHOUSE_USER}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
CLICKHOUSE_DB: ${CLICKHOUSE_DATABASE}
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
networks:
- oneuptime
volumes:
- clickhouse:/var/lib/clickhouse/
healthcheck:
test: ["CMD-SHELL", "clickhouse-client --query 'SELECT 1'"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
logging:
driver: "local"
options:
max-size: "1000m"
postgres:
image: postgres:15
restart: always
environment:
POSTGRES_USER: ${DATABASE_USERNAME}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
POSTGRES_DB: ${DATABASE_NAME}
networks:
- oneuptime
volumes:
- postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
logging:
driver: "local"
options:
max-size: "1000m"
postgres:
image: postgres:15
restart: always
environment:
POSTGRES_USER: ${DATABASE_USERNAME}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
POSTGRES_DB: ${DATABASE_NAME}
networks:
- oneuptime
volumes:
- postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
logging:
driver: "local"
options:
max-size: "1000m"
llm:
networks:
- oneuptime
restart: always
environment:
<<: *common-runtime-variables
PORT: 8547
HF_TOKEN: ${LLM_SERVER_HUGGINGFACE_TOKEN}
HF_MODEL_NAME: ${LLM_SERVER_HUGGINGFACE_MODEL_NAME}
volumes:
- ./LLM/Models:/app/Models
logging:
driver: "local"
options:
max-size: "1000m"
llm:
networks:
- oneuptime
restart: always
environment:
<<: *common-runtime-variables
PORT: 8547
HF_TOKEN: ${LLM_SERVER_HUGGINGFACE_TOKEN}
HF_MODEL_NAME: ${LLM_SERVER_HUGGINGFACE_MODEL_NAME}
volumes:
- ./LLM/Models:/app/Models
logging:
driver: "local"
options:
max-size: "1000m"
test-server:
networks:
- oneuptime
restart: always
environment:
<<: *common-runtime-variables
PORT: ${TEST_SERVER_PORT}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_TEST_SERVER}
logging:
driver: "local"
options:
max-size: "1000m"
test-server:
networks:
- oneuptime
restart: always
environment:
<<: *common-runtime-variables
PORT: ${TEST_SERVER_PORT}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_TEST_SERVER}
logging:
driver: "local"
options:
max-size: "1000m"
app:
networks:
- oneuptime
restart: always
environment:
<<: *common-runtime-variables
PORT: ${APP_PORT}
SMS_DEFAULT_COST_IN_CENTS: ${SMS_DEFAULT_COST_IN_CENTS}
WHATSAPP_TEXT_DEFAULT_COST_IN_CENTS: ${WHATSAPP_TEXT_DEFAULT_COST_IN_CENTS}
CALL_DEFAULT_COST_IN_CENTS_PER_MINUTE: ${CALL_DEFAULT_COST_IN_CENTS_PER_MINUTE}
SMS_HIGH_RISK_COST_IN_CENTS: ${SMS_HIGH_RISK_COST_IN_CENTS}
CALL_HIGH_RISK_COST_IN_CENTS_PER_MINUTE: ${CALL_HIGH_RISK_COST_IN_CENTS_PER_MINUTE}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_APP}
logging:
driver: "local"
options:
max-size: "1000m"
app:
networks:
- oneuptime
restart: always
environment:
<<: *common-runtime-variables
PORT: ${APP_PORT}
SMS_DEFAULT_COST_IN_CENTS: ${SMS_DEFAULT_COST_IN_CENTS}
WHATSAPP_TEXT_DEFAULT_COST_IN_CENTS: ${WHATSAPP_TEXT_DEFAULT_COST_IN_CENTS}
CALL_DEFAULT_COST_IN_CENTS_PER_MINUTE: ${CALL_DEFAULT_COST_IN_CENTS_PER_MINUTE}
SMS_HIGH_RISK_COST_IN_CENTS: ${SMS_HIGH_RISK_COST_IN_CENTS}
CALL_HIGH_RISK_COST_IN_CENTS_PER_MINUTE: ${CALL_HIGH_RISK_COST_IN_CENTS_PER_MINUTE}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_APP}
logging:
driver: "local"
options:
max-size: "1000m"
home:
networks:
- oneuptime
restart: always
environment:
<<: *common-runtime-variables
PORT: ${HOME_PORT}
logging:
driver: "local"
options:
max-size: "1000m"
home:
networks:
- oneuptime
restart: always
environment:
<<: *common-runtime-variables
PORT: ${HOME_PORT}
logging:
driver: "local"
options:
max-size: "1000m"
worker:
networks:
- oneuptime
restart: always
environment:
<<: *common-runtime-variables
PORT: ${WORKER_PORT}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_WORKER}
WORKER_CONCURRENCY: ${WORKER_CONCURRENCY}
logging:
driver: "local"
options:
max-size: "1000m"
worker:
networks:
- oneuptime
restart: always
environment:
<<: *common-runtime-variables
PORT: ${WORKER_PORT}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_WORKER}
WORKER_CONCURRENCY: ${WORKER_CONCURRENCY}
logging:
driver: "local"
options:
max-size: "1000m"
probe-1:
restart: always
network_mode: host
environment:
ONEUPTIME_URL: ${GLOBAL_PROBE_1_ONEUPTIME_URL}
REGISTER_PROBE_KEY: ${REGISTER_PROBE_KEY}
PROBE_NAME: ${GLOBAL_PROBE_1_NAME}
PROBE_DESCRIPTION: ${GLOBAL_PROBE_1_DESCRIPTION}
PROBE_MONITORING_WORKERS: ${GLOBAL_PROBE_1_MONITORING_WORKERS}
PROBE_KEY: ${GLOBAL_PROBE_1_KEY}
PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS: ${GLOBAL_PROBE_1_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS}
PROBE_CUSTOM_CODE_MONITOR_SCRIPT_TIMEOUT_IN_MS: ${GLOBAL_PROBE_1_CUSTOM_CODE_MONITOR_SCRIPT_TIMEOUT_IN_MS}
PROBE_MONITOR_FETCH_LIMIT: ${GLOBAL_PROBE_1_MONITOR_FETCH_LIMIT}
PORT: ${GLOBAL_PROBE_1_PORT}
NODE_ENV: ${ENVIRONMENT}
LOG_LEVEL: ${LOG_LEVEL}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_PROBE}
OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT: ${OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT}
OPENTELEMETRY_EXPORTER_OTLP_HEADERS: ${OPENTELEMETRY_EXPORTER_OTLP_HEADERS}
logging:
driver: "local"
options:
max-size: "1000m"
probe-1:
restart: always
network_mode: host
environment:
ONEUPTIME_URL: ${GLOBAL_PROBE_1_ONEUPTIME_URL}
REGISTER_PROBE_KEY: ${REGISTER_PROBE_KEY}
PROBE_NAME: ${GLOBAL_PROBE_1_NAME}
PROBE_DESCRIPTION: ${GLOBAL_PROBE_1_DESCRIPTION}
PROBE_MONITORING_WORKERS: ${GLOBAL_PROBE_1_MONITORING_WORKERS}
PROBE_KEY: ${GLOBAL_PROBE_1_KEY}
PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS: ${GLOBAL_PROBE_1_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS}
PROBE_CUSTOM_CODE_MONITOR_SCRIPT_TIMEOUT_IN_MS: ${GLOBAL_PROBE_1_CUSTOM_CODE_MONITOR_SCRIPT_TIMEOUT_IN_MS}
PROBE_MONITOR_FETCH_LIMIT: ${GLOBAL_PROBE_1_MONITOR_FETCH_LIMIT}
PROBE_SYNTHETIC_RUNNER_URL: http://127.0.0.1:${GLOBAL_PROBE_1_SYNTHETIC_RUNNER_PORT:-3885}
HTTP_PROXY_URL: ${GLOBAL_PROBE_1_PROXY_URL}
HTTPS_PROXY_URL: ${GLOBAL_PROBE_1_PROXY_URL}
PORT: ${GLOBAL_PROBE_1_PORT}
NODE_ENV: ${ENVIRONMENT}
LOG_LEVEL: ${LOG_LEVEL}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_PROBE}
OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT: ${OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT}
OPENTELEMETRY_EXPORTER_OTLP_HEADERS: ${OPENTELEMETRY_EXPORTER_OTLP_HEADERS}
logging:
driver: "local"
options:
max-size: "1000m"
probe-2:
restart: always
network_mode: host
environment:
ONEUPTIME_URL: ${GLOBAL_PROBE_2_ONEUPTIME_URL}
REGISTER_PROBE_KEY: ${REGISTER_PROBE_KEY}
PROBE_NAME: ${GLOBAL_PROBE_2_NAME}
PROBE_DESCRIPTION: ${GLOBAL_PROBE_2_DESCRIPTION}
PROBE_MONITORING_WORKERS: ${GLOBAL_PROBE_2_MONITORING_WORKERS}
PROBE_KEY: ${GLOBAL_PROBE_2_KEY}
PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS: ${GLOBAL_PROBE_2_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS}
PROBE_CUSTOM_CODE_MONITOR_SCRIPT_TIMEOUT_IN_MS: ${GLOBAL_PROBE_2_CUSTOM_CODE_MONITOR_SCRIPT_TIMEOUT_IN_MS}
PROBE_MONITOR_FETCH_LIMIT: ${GLOBAL_PROBE_2_MONITOR_FETCH_LIMIT}
PORT: ${GLOBAL_PROBE_2_PORT}
NODE_ENV: ${ENVIRONMENT}
LOG_LEVEL: ${LOG_LEVEL}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_PROBE}
OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT: ${OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT}
OPENTELEMETRY_EXPORTER_OTLP_HEADERS: ${OPENTELEMETRY_EXPORTER_OTLP_HEADERS}
logging:
driver: "local"
options:
max-size: "1000m"
probe-2:
restart: always
network_mode: host
environment:
ONEUPTIME_URL: ${GLOBAL_PROBE_2_ONEUPTIME_URL}
REGISTER_PROBE_KEY: ${REGISTER_PROBE_KEY}
PROBE_NAME: ${GLOBAL_PROBE_2_NAME}
PROBE_DESCRIPTION: ${GLOBAL_PROBE_2_DESCRIPTION}
PROBE_MONITORING_WORKERS: ${GLOBAL_PROBE_2_MONITORING_WORKERS}
PROBE_KEY: ${GLOBAL_PROBE_2_KEY}
PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS: ${GLOBAL_PROBE_2_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS}
PROBE_CUSTOM_CODE_MONITOR_SCRIPT_TIMEOUT_IN_MS: ${GLOBAL_PROBE_2_CUSTOM_CODE_MONITOR_SCRIPT_TIMEOUT_IN_MS}
PROBE_MONITOR_FETCH_LIMIT: ${GLOBAL_PROBE_2_MONITOR_FETCH_LIMIT}
PROBE_SYNTHETIC_RUNNER_URL: http://127.0.0.1:${GLOBAL_PROBE_2_SYNTHETIC_RUNNER_PORT:-3886}
HTTP_PROXY_URL: ${GLOBAL_PROBE_2_PROXY_URL}
HTTPS_PROXY_URL: ${GLOBAL_PROBE_2_PROXY_URL}
PORT: ${GLOBAL_PROBE_2_PORT}
NODE_ENV: ${ENVIRONMENT}
LOG_LEVEL: ${LOG_LEVEL}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_PROBE}
OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT: ${OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT}
OPENTELEMETRY_EXPORTER_OTLP_HEADERS: ${OPENTELEMETRY_EXPORTER_OTLP_HEADERS}
logging:
driver: "local"
options:
max-size: "1000m"
ai-agent:
restart: always
network_mode: host
environment:
<<: *common-runtime-variables
AI_AGENT_KEY: ${AI_AGENT_KEY}
ONEUPTIME_URL: ${AI_AGENT_ONEUPTIME_URL}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_AI_AGENT}
PORT: ${AI_AGENT_PORT}
logging:
driver: "local"
options:
max-size: "1000m"
synthetic-runner-1:
restart: always
network_mode: host
command: npm run start:synthetic-runner
environment:
ONEUPTIME_SECRET: ${ONEUPTIME_SECRET}
PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS: ${GLOBAL_PROBE_1_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS}
HTTP_PROXY_URL: ${GLOBAL_PROBE_1_PROXY_URL}
HTTPS_PROXY_URL: ${GLOBAL_PROBE_1_PROXY_URL}
PORT: ${GLOBAL_PROBE_1_SYNTHETIC_RUNNER_PORT:-3885}
NODE_ENV: ${ENVIRONMENT}
LOG_LEVEL: ${LOG_LEVEL}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_PROBE}
OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT: ${OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT}
OPENTELEMETRY_EXPORTER_OTLP_HEADERS: ${OPENTELEMETRY_EXPORTER_OTLP_HEADERS}
logging:
driver: "local"
options:
max-size: "1000m"
fluentd:
networks:
- oneuptime
restart: always
logging:
driver: "local"
options:
max-size: "1000m"
fluent-bit:
networks:
- oneuptime
restart: always
logging:
driver: "local"
options:
max-size: "1000m"
synthetic-runner-2:
restart: always
network_mode: host
command: npm run start:synthetic-runner
environment:
ONEUPTIME_SECRET: ${ONEUPTIME_SECRET}
PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS: ${GLOBAL_PROBE_2_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS}
HTTP_PROXY_URL: ${GLOBAL_PROBE_2_PROXY_URL}
HTTPS_PROXY_URL: ${GLOBAL_PROBE_2_PROXY_URL}
PORT: ${GLOBAL_PROBE_2_SYNTHETIC_RUNNER_PORT:-3886}
NODE_ENV: ${ENVIRONMENT}
LOG_LEVEL: ${LOG_LEVEL}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_PROBE}
OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT: ${OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT}
OPENTELEMETRY_EXPORTER_OTLP_HEADERS: ${OPENTELEMETRY_EXPORTER_OTLP_HEADERS}
logging:
driver: "local"
options:
max-size: "1000m"
telemetry:
networks:
- oneuptime
restart: always
environment:
<<: *common-runtime-variables
PORT: ${TELEMETRY_PORT}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_TELEMETRY}
# Max concurrent telemetry jobs the worker will process
TELEMETRY_CONCURRENCY: ${TELEMETRY_CONCURRENCY}
REGISTER_PROBE_KEY: ${REGISTER_PROBE_KEY}
logging:
driver: "local"
options:
max-size: "1000m"
e2e:
restart: "no"
network_mode: host # This is needed to access the host network,
environment:
<<: *common-variables
E2E_TEST_IS_USER_REGISTERED: ${E2E_TEST_IS_USER_REGISTERED}
E2E_TEST_REGISTERED_USER_EMAIL: ${E2E_TEST_REGISTERED_USER_EMAIL}
E2E_TEST_REGISTERED_USER_PASSWORD: ${E2E_TEST_REGISTERED_USER_PASSWORD}
E2E_TEST_STATUS_PAGE_URL: ${E2E_TEST_STATUS_PAGE_URL}
E2E_TESTS_FAILED_WEBHOOK_URL: ${E2E_TESTS_FAILED_WEBHOOK_URL}
logging:
driver: "local"
options:
max-size: "1000m"
ai-agent:
restart: always
network_mode: host
environment:
<<: *common-runtime-variables
AI_AGENT_KEY: ${AI_AGENT_KEY}
ONEUPTIME_URL: ${AI_AGENT_ONEUPTIME_URL}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_AI_AGENT}
PORT: ${AI_AGENT_PORT}
logging:
driver: "local"
options:
max-size: "1000m"
ingress:
restart: always
networks:
- oneuptime
environment:
<<: *common-runtime-variables
ONEUPTIME_HTTP_PORT: ${ONEUPTIME_HTTP_PORT}
NGINX_LISTEN_ADDRESS: ${NGINX_LISTEN_ADDRESS}
NGINX_LISTEN_OPTIONS: ${NGINX_LISTEN_OPTIONS}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_INGRESS}
ports:
- '${ONEUPTIME_HTTP_PORT}:7849'
- '${STATUS_PAGE_HTTPS_PORT}:7850'
logging:
driver: "local"
options:
max-size: "1000m"
fluentd:
networks:
- oneuptime
restart: always
logging:
driver: "local"
options:
max-size: "1000m"
fluent-bit:
networks:
- oneuptime
restart: always
logging:
driver: "local"
options:
max-size: "1000m"
telemetry:
networks:
- oneuptime
restart: always
environment:
<<: *common-runtime-variables
PORT: ${TELEMETRY_PORT}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_TELEMETRY}
# Max concurrent telemetry jobs the worker will process
TELEMETRY_CONCURRENCY: ${TELEMETRY_CONCURRENCY}
REGISTER_PROBE_KEY: ${REGISTER_PROBE_KEY}
logging:
driver: "local"
options:
max-size: "1000m"
e2e:
restart: "no"
network_mode: host # This is needed to access the host network,
environment:
<<: *common-variables
E2E_TEST_IS_USER_REGISTERED: ${E2E_TEST_IS_USER_REGISTERED}
E2E_TEST_REGISTERED_USER_EMAIL: ${E2E_TEST_REGISTERED_USER_EMAIL}
E2E_TEST_REGISTERED_USER_PASSWORD: ${E2E_TEST_REGISTERED_USER_PASSWORD}
E2E_TEST_STATUS_PAGE_URL: ${E2E_TEST_STATUS_PAGE_URL}
E2E_TESTS_FAILED_WEBHOOK_URL: ${E2E_TESTS_FAILED_WEBHOOK_URL}
logging:
driver: "local"
options:
max-size: "1000m"
ingress:
restart: always
networks:
- oneuptime
environment:
<<: *common-runtime-variables
ONEUPTIME_HTTP_PORT: ${ONEUPTIME_HTTP_PORT}
NGINX_LISTEN_ADDRESS: ${NGINX_LISTEN_ADDRESS}
NGINX_LISTEN_OPTIONS: ${NGINX_LISTEN_OPTIONS}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_INGRESS}
ports:
- "${ONEUPTIME_HTTP_PORT}:7849"
- "${STATUS_PAGE_HTTPS_PORT}:7850"
logging:
driver: "local"
options:
max-size: "1000m"
volumes:
postgres:
clickhouse:
postgres:
clickhouse:
networks:
oneuptime:

View File

@@ -2,84 +2,88 @@
# It extends docker-compose.yml with the home container.
x-common-depends-on: &common-depends-on
postgres:
condition: service_healthy
redis:
condition: service_healthy
clickhouse:
condition: service_healthy
postgres:
condition: service_healthy
redis:
condition: service_healthy
clickhouse:
condition: service_healthy
services:
redis:
extends:
file: ./docker-compose.base.yml
service: redis
redis:
extends:
file: ./docker-compose.base.yml
service: redis
clickhouse:
extends:
file: ./docker-compose.base.yml
service: clickhouse
clickhouse:
extends:
file: ./docker-compose.base.yml
service: clickhouse
postgres:
ports:
- "5400:5432"
extends:
file: ./docker-compose.base.yml
service: postgres
postgres:
ports:
- "5400:5432"
extends:
file: ./docker-compose.base.yml
service: postgres
app:
image: oneuptime/app:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: app
depends_on:
<<: *common-depends-on
home:
image: oneuptime/home:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: home
depends_on:
<<: *common-depends-on
app:
image: oneuptime/app:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: app
depends_on:
<<: *common-depends-on
worker:
image: oneuptime/worker:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: worker
depends_on:
<<: *common-depends-on
home:
image: oneuptime/home:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: home
depends_on:
<<: *common-depends-on
probe-1:
image: oneuptime/probe:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: probe-1
depends_on:
<<: *common-depends-on
worker:
image: oneuptime/worker:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: worker
depends_on:
<<: *common-depends-on
synthetic-runner-1:
image: oneuptime/probe:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: synthetic-runner-1
probe-1:
image: oneuptime/probe:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: probe-1
depends_on:
<<: *common-depends-on
telemetry:
image: oneuptime/telemetry:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: telemetry
depends_on:
<<: *common-depends-on
telemetry:
image: oneuptime/telemetry:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: telemetry
depends_on:
<<: *common-depends-on
ingress:
image: oneuptime/nginx:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: ingress
depends_on:
<<: *common-depends-on
ingress:
image: oneuptime/nginx:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: ingress
depends_on:
<<: *common-depends-on
volumes:
postgres:
clickhouse:
postgres:
clickhouse:
networks:
oneuptime:

View File

@@ -1,281 +1,297 @@
x-common-depends-on: &common-depends-on
postgres:
condition: service_healthy
redis:
condition: service_healthy
clickhouse:
condition: service_healthy
postgres:
condition: service_healthy
redis:
condition: service_healthy
clickhouse:
condition: service_healthy
services:
redis:
ports:
- "6310:6379"
extends:
file: ./docker-compose.base.yml
service: redis
clickhouse:
ports:
- "9034:9000"
- "8189:8123"
extends:
file: ./docker-compose.base.yml
service: clickhouse
volumes:
- ./Clickhouse/config.xml:/etc/clickhouse-server/config.xml
redis:
ports:
- '6310:6379'
extends:
file: ./docker-compose.base.yml
service: redis
postgres:
ports:
- "5400:5432"
extends:
file: ./docker-compose.base.yml
service: postgres
clickhouse:
ports:
- '9034:9000'
- '8189:8123'
extends:
file: ./docker-compose.base.yml
service: clickhouse
volumes:
- ./Clickhouse/config.xml:/etc/clickhouse-server/config.xml
test-server:
volumes:
- ./TestServer:/usr/src/app:cached
# Use node modules of the container and not host system.
# https://stackoverflow.com/questions/29181032/add-a-volume-to-docker-but-exclude-a-sub-folder
- /usr/src/app/node_modules/
- ./Common:/usr/src/Common:cached
postgres:
ports:
- '5400:5432'
extends:
file: ./docker-compose.base.yml
service: postgres
- /usr/src/Common/node_modules/
extends:
file: ./docker-compose.base.yml
service: test-server
depends_on:
<<: *common-depends-on
ports:
- "9141:9229" # Debugging port.
- "3800:3800"
build:
network: host
context: .
dockerfile: ./TestServer/Dockerfile
test-server:
volumes:
- ./TestServer:/usr/src/app:cached
# Use node modules of the container and not host system.
# https://stackoverflow.com/questions/29181032/add-a-volume-to-docker-but-exclude-a-sub-folder
- /usr/src/app/node_modules/
- ./Common:/usr/src/Common:cached
home:
volumes:
- ./Home:/usr/src/app:cached
# Use node modules of the container and not host system.
# https://stackoverflow.com/questions/29181032/add-a-volume-to-docker-but-exclude-a-sub-folder
- /usr/src/app/node_modules/
- ./Common:/usr/src/Common:cached
- /usr/src/Common/node_modules/
extends:
file: ./docker-compose.base.yml
service: home
depends_on:
<<: *common-depends-on
ports:
- "9212:9229" # Debugging port.
build:
network: host
context: .
dockerfile: ./Home/Dockerfile
- /usr/src/Common/node_modules/
worker:
volumes:
- ./Worker:/usr/src/app:cached
# Use node modules of the container and not host system.
# https://stackoverflow.com/questions/29181032/add-a-volume-to-docker-but-exclude-a-sub-folder
- /usr/src/app/node_modules/
- ./Common:/usr/src/Common:cached
- /usr/src/Common/node_modules/
extends:
file: ./docker-compose.base.yml
service: worker
depends_on:
<<: *common-depends-on
ports:
- "8734:9229" # Debugging port.
build:
network: host
context: .
dockerfile: ./Worker/Dockerfile
app:
volumes:
- ./App:/usr/src/app:cached
- ./App/FeatureSet/Accounts:/usr/src/app/FeatureSet/Accounts:cached
- ./App/FeatureSet/Dashboard:/usr/src/app/FeatureSet/Dashboard:cached
- ./App/FeatureSet/AdminDashboard:/usr/src/app/FeatureSet/AdminDashboard:cached
- ./App/FeatureSet/StatusPage:/usr/src/app/FeatureSet/StatusPage:cached
# Use node modules of the container and not host system.
# https://stackoverflow.com/questions/29181032/add-a-volume-to-docker-but-exclude-a-sub-folder
- /usr/src/app/node_modules/
- /usr/src/app/FeatureSet/Accounts/node_modules/
- /usr/src/app/FeatureSet/Dashboard/node_modules/
- /usr/src/app/FeatureSet/AdminDashboard/node_modules/
- /usr/src/app/FeatureSet/StatusPage/node_modules/
- ./Common:/usr/src/Common:cached
- /usr/src/Common/node_modules/
extends:
file: ./docker-compose.base.yml
service: app
depends_on:
<<: *common-depends-on
ports:
- "9232:9229" # Debugging port.
build:
network: host
context: .
dockerfile: ./App/Dockerfile
extends:
file: ./docker-compose.base.yml
service: test-server
depends_on:
<<: *common-depends-on
ports:
- '9141:9229' # Debugging port.
- '3800:3800'
build:
network: host
context: .
dockerfile: ./TestServer/Dockerfile
probe-1:
volumes:
- ./Probe:/usr/src/app:cached
# Use node modules of the container and not host system.
# https://stackoverflow.com/questions/29181032/add-a-volume-to-docker-but-exclude-a-sub-folder
- /usr/src/app/node_modules/
- ./Common:/usr/src/Common:cached
- /usr/src/Common/node_modules/
home:
volumes:
- ./Home:/usr/src/app:cached
# Use node modules of the container and not host system.
# https://stackoverflow.com/questions/29181032/add-a-volume-to-docker-but-exclude-a-sub-folder
- /usr/src/app/node_modules/
- ./Common:/usr/src/Common:cached
- /usr/src/Common/node_modules/
extends:
file: ./docker-compose.base.yml
service: home
depends_on:
<<: *common-depends-on
ports:
- '9212:9229' # Debugging port.
build:
network: host
context: .
dockerfile: ./Home/Dockerfile
extends:
file: ./docker-compose.base.yml
service: probe-1
depends_on:
<<: *common-depends-on
build:
network: host
context: .
dockerfile: ./Probe/Dockerfile
worker:
volumes:
- ./Worker:/usr/src/app:cached
# Use node modules of the container and not host system.
# https://stackoverflow.com/questions/29181032/add-a-volume-to-docker-but-exclude-a-sub-folder
- /usr/src/app/node_modules/
- ./Common:/usr/src/Common:cached
- /usr/src/Common/node_modules/
extends:
file: ./docker-compose.base.yml
service: worker
depends_on:
<<: *common-depends-on
ports:
- '8734:9229' # Debugging port.
build:
network: host
context: .
dockerfile: ./Worker/Dockerfile
synthetic-runner-1:
volumes:
- ./Probe:/usr/src/app:cached
- /usr/src/app/node_modules/
- ./Common:/usr/src/Common:cached
- /usr/src/Common/node_modules/
extends:
file: ./docker-compose.base.yml
service: synthetic-runner-1
build:
network: host
context: .
dockerfile: ./Probe/Dockerfile
probe-2:
volumes:
- ./Probe:/usr/src/app:cached
# Use node modules of the container and not host system.
# https://stackoverflow.com/questions/29181032/add-a-volume-to-docker-but-exclude-a-sub-folder
- /usr/src/app/node_modules/
- ./Common:/usr/src/Common:cached
app:
volumes:
- ./App:/usr/src/app:cached
- ./App/FeatureSet/Accounts:/usr/src/app/FeatureSet/Accounts:cached
- ./App/FeatureSet/Dashboard:/usr/src/app/FeatureSet/Dashboard:cached
- ./App/FeatureSet/AdminDashboard:/usr/src/app/FeatureSet/AdminDashboard:cached
- ./App/FeatureSet/StatusPage:/usr/src/app/FeatureSet/StatusPage:cached
# Use node modules of the container and not host system.
# https://stackoverflow.com/questions/29181032/add-a-volume-to-docker-but-exclude-a-sub-folder
- /usr/src/app/node_modules/
- /usr/src/app/FeatureSet/Accounts/node_modules/
- /usr/src/app/FeatureSet/Dashboard/node_modules/
- /usr/src/app/FeatureSet/AdminDashboard/node_modules/
- /usr/src/app/FeatureSet/StatusPage/node_modules/
- ./Common:/usr/src/Common:cached
- /usr/src/Common/node_modules/
extends:
file: ./docker-compose.base.yml
service: app
depends_on:
<<: *common-depends-on
ports:
- '9232:9229' # Debugging port.
build:
network: host
context: .
dockerfile: ./App/Dockerfile
- /usr/src/Common/node_modules/
extends:
file: ./docker-compose.base.yml
service: probe-2
depends_on:
<<: *common-depends-on
build:
network: host
context: .
dockerfile: ./Probe/Dockerfile
probe-1:
volumes:
- ./Probe:/usr/src/app:cached
# Use node modules of the container and not host system.
# https://stackoverflow.com/questions/29181032/add-a-volume-to-docker-but-exclude-a-sub-folder
- /usr/src/app/node_modules/
- ./Common:/usr/src/Common:cached
synthetic-runner-2:
volumes:
- ./Probe:/usr/src/app:cached
- /usr/src/app/node_modules/
- ./Common:/usr/src/Common:cached
- /usr/src/Common/node_modules/
extends:
file: ./docker-compose.base.yml
service: synthetic-runner-2
build:
network: host
context: .
dockerfile: ./Probe/Dockerfile
- /usr/src/Common/node_modules/
ai-agent:
volumes:
- ./AIAgent:/usr/src/app:cached
# Use node modules of the container and not host system.
# https://stackoverflow.com/questions/29181032/add-a-volume-to-docker-but-exclude-a-sub-folder
- /usr/src/app/node_modules/
- ./Common:/usr/src/Common:cached
- /usr/src/Common/node_modules/
extends:
file: ./docker-compose.base.yml
service: probe-1
depends_on:
<<: *common-depends-on
build:
network: host
context: .
dockerfile: ./Probe/Dockerfile
extends:
file: ./docker-compose.base.yml
service: ai-agent
depends_on:
<<: *common-depends-on
build:
network: host
context: .
dockerfile: ./AIAgent/Dockerfile
probe-2:
volumes:
- ./Probe:/usr/src/app:cached
# Use node modules of the container and not host system.
# https://stackoverflow.com/questions/29181032/add-a-volume-to-docker-but-exclude-a-sub-folder
- /usr/src/app/node_modules/
- ./Common:/usr/src/Common:cached
telemetry:
volumes:
- ./Telemetry:/usr/src/app:cached
# Use node modules of the container and not host system.
# https://stackoverflow.com/questions/29181032/add-a-volume-to-docker-but-exclude-a-sub-folder
- /usr/src/app/node_modules/
- ./Common:/usr/src/Common:cached
- /usr/src/Common/node_modules/
ports:
- "9938:9229" # Debugging port.
extends:
file: ./docker-compose.base.yml
service: telemetry
depends_on:
<<: *common-depends-on
build:
network: host
context: .
dockerfile: ./Telemetry/Dockerfile
- /usr/src/Common/node_modules/
# Fluentd. Required only for development. In production its the responsibility of the customer to run fluentd and pipe logs to OneUptime.
# We run this container just for development, to see if logs are piped.
fluentd:
ports:
- 24224:24224
- 24224:24224/udp
- 8888:8888
user: fluent
extends:
file: ./docker-compose.base.yml
service: fluentd
volumes:
- ./Fluentd/fluent.conf:/fluentd/etc/fluent.conf
build:
network: host
context: ./Fluentd
dockerfile: ./Dockerfile
extends:
file: ./docker-compose.base.yml
service: probe-2
depends_on:
<<: *common-depends-on
build:
network: host
context: .
dockerfile: ./Probe/Dockerfile
fluent-bit:
ports:
- 24225:24224
- 24285:24284
- 2020:2020
- 8889:8889
extends:
file: ./docker-compose.base.yml
service: fluent-bit
volumes:
- ./FluentBit/etc:/fluent-bit/etc/
build:
network: host
context: .
dockerfile: ./FluentBit/Dockerfile
ai-agent:
volumes:
- ./AIAgent:/usr/src/app:cached
# Use node modules of the container and not host system.
# https://stackoverflow.com/questions/29181032/add-a-volume-to-docker-but-exclude-a-sub-folder
- /usr/src/app/node_modules/
- ./Common:/usr/src/Common:cached
ingress:
build:
network: host
context: .
dockerfile: ./Nginx/Dockerfile
extends:
file: ./docker-compose.base.yml
service: ingress
depends_on:
<<: *common-depends-on
- /usr/src/Common/node_modules/
extends:
file: ./docker-compose.base.yml
service: ai-agent
depends_on:
<<: *common-depends-on
build:
network: host
context: .
dockerfile: ./AIAgent/Dockerfile
telemetry:
volumes:
- ./Telemetry:/usr/src/app:cached
# Use node modules of the container and not host system.
# https://stackoverflow.com/questions/29181032/add-a-volume-to-docker-but-exclude-a-sub-folder
- /usr/src/app/node_modules/
- ./Common:/usr/src/Common:cached
- /usr/src/Common/node_modules/
ports:
- '9938:9229' # Debugging port.
extends:
file: ./docker-compose.base.yml
service: telemetry
depends_on:
<<: *common-depends-on
build:
network: host
context: .
dockerfile: ./Telemetry/Dockerfile
# Fluentd. Required only for development. In production its the responsibility of the customer to run fluentd and pipe logs to OneUptime.
# We run this container just for development, to see if logs are piped.
fluentd:
ports:
- 24224:24224
- 24224:24224/udp
- 8888:8888
user: fluent
extends:
file: ./docker-compose.base.yml
service: fluentd
volumes:
- ./Fluentd/fluent.conf:/fluentd/etc/fluent.conf
build:
network: host
context: ./Fluentd
dockerfile: ./Dockerfile
fluent-bit:
ports:
- 24225:24224
- 24285:24284
- 2020:2020
- 8889:8889
extends:
file: ./docker-compose.base.yml
service: fluent-bit
volumes:
- ./FluentBit/etc:/fluent-bit/etc/
build:
network: host
context: .
dockerfile: ./FluentBit/Dockerfile
ingress:
build:
network: host
context: .
dockerfile: ./Nginx/Dockerfile
extends:
file: ./docker-compose.base.yml
service: ingress
depends_on:
<<: *common-depends-on
# e2e tests
e2e:
extends:
file: ./docker-compose.base.yml
service: e2e
volumes:
- ./E2E/playwright-report:/usr/src/app/playwright-report
- ./E2E/test-results:/usr/src/app/test-results
build:
network: host
context: .
dockerfile: ./E2E/Dockerfile
# e2e tests
e2e:
extends:
file: ./docker-compose.base.yml
service: e2e
volumes:
- ./E2E/playwright-report:/usr/src/app/playwright-report
- ./E2E/test-results:/usr/src/app/test-results
build:
network: host
context: .
dockerfile: ./E2E/Dockerfile
volumes:
postgres:
clickhouse:
postgres:
clickhouse:
networks:
oneuptime:

View File

@@ -2,76 +2,80 @@
# For example, if you want to use the image from Github Container Registry, you can change the image tag from oneuptime/dashboard:latest to ghcr.io/oneuptime/dashboard:latest
x-common-depends-on: &common-depends-on
postgres:
condition: service_healthy
redis:
condition: service_healthy
clickhouse:
condition: service_healthy
postgres:
condition: service_healthy
redis:
condition: service_healthy
clickhouse:
condition: service_healthy
services:
redis:
extends:
file: ./docker-compose.base.yml
service: redis
redis:
extends:
file: ./docker-compose.base.yml
service: redis
clickhouse:
extends:
file: ./docker-compose.base.yml
service: clickhouse
clickhouse:
extends:
file: ./docker-compose.base.yml
service: clickhouse
postgres:
ports:
- "5400:5432" # for access to postgres for backups. If you don't need backup, you can comment this line out to make it more secure.
extends:
file: ./docker-compose.base.yml
service: postgres
postgres:
ports:
- "5400:5432" # for access to postgres for backups. If you don't need backup, you can comment this line out to make it more secure.
extends:
file: ./docker-compose.base.yml
service: postgres
app:
image: oneuptime/app:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: app
depends_on:
<<: *common-depends-on
worker:
image: oneuptime/worker:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: worker
depends_on:
<<: *common-depends-on
app:
image: oneuptime/app:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: app
depends_on:
<<: *common-depends-on
probe-1:
image: oneuptime/probe:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: probe-1
depends_on:
<<: *common-depends-on
worker:
image: oneuptime/worker:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: worker
depends_on:
<<: *common-depends-on
synthetic-runner-1:
image: oneuptime/probe:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: synthetic-runner-1
probe-1:
image: oneuptime/probe:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: probe-1
depends_on:
<<: *common-depends-on
telemetry:
image: oneuptime/telemetry:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: telemetry
depends_on:
<<: *common-depends-on
telemetry:
image: oneuptime/telemetry:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: telemetry
depends_on:
<<: *common-depends-on
ingress:
image: oneuptime/nginx:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: ingress
depends_on:
<<: *common-depends-on
ingress:
image: oneuptime/nginx:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: ingress
depends_on:
<<: *common-depends-on
volumes:
postgres:
clickhouse:
postgres:
clickhouse:
networks:
oneuptime: