diff --git a/client/src/Pages/v1/Infrastructure/Create/index.jsx b/client/src/Pages/v1/Infrastructure/Create/index.jsx
index d4fd859ec..82cf80c1b 100644
--- a/client/src/Pages/v1/Infrastructure/Create/index.jsx
+++ b/client/src/Pages/v1/Infrastructure/Create/index.jsx
@@ -13,7 +13,7 @@ import CustomAlertsSection from "./Components/CustomAlertsSection.jsx";
// Utils
import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx";
import { useGetNotificationsByTeamId } from "../../../../Hooks/v1/useNotifications.js";
-import { useParams } from "react-router-dom";
+import { useParams, useSearchParams } from "react-router-dom";
import { useState, useEffect } from "react";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
@@ -29,11 +29,15 @@ import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit.jsx";
const CreateInfrastructureMonitor = () => {
const { monitorId } = useParams();
+ const [searchParams] = useSearchParams();
const isCreate = typeof monitorId === "undefined";
const theme = useTheme();
const { t } = useTranslation();
+ // Get monitor type from query params (default to hardware for backwards compatibility)
+ const monitorType = searchParams.get("type") || "hardware";
+
// State
const [https, setHttps] = useState(false);
const [updateTrigger, setUpdateTrigger] = useState(false);
diff --git a/client/src/Pages/v1/Infrastructure/Monitors/index.jsx b/client/src/Pages/v1/Infrastructure/Monitors/index.jsx
index 6c3a657ab..bef4330e9 100644
--- a/client/src/Pages/v1/Infrastructure/Monitors/index.jsx
+++ b/client/src/Pages/v1/Infrastructure/Monitors/index.jsx
@@ -1,5 +1,6 @@
// Components
-import { Stack } from "@mui/material";
+import { Stack, Tab } from "@mui/material";
+import { TabContext, TabPanel } from "@mui/lab";
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
import MonitorCountHeader from "@/Components/v1/MonitorCountHeader/index.jsx";
import MonitorCreateHeader from "@/Components/v1/MonitorCreateHeader/index.jsx";
@@ -8,6 +9,7 @@ import Pagination from "@/Components/v1/Table/TablePagination/index.jsx";
import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx";
import Filter from "./Components/Filters/index.jsx";
import SearchComponent from "../../Uptime/Monitors/Components/SearchComponent/index.jsx";
+import CustomTabList from "@/Components/v1/Tab/index.jsx";
// Utils
import { useTheme } from "@emotion/react";
import { useEffect, useState } from "react";
@@ -17,7 +19,6 @@ import { useFetchMonitorsByTeamId } from "../../../../Hooks/v1/monitorHooks.js";
import { useDispatch, useSelector } from "react-redux";
import { setRowsPerPage } from "../../../../Features/UI/uiSlice.js";
// Constants
-const TYPES = ["hardware"];
const BREADCRUMBS = [{ name: `infrastructure`, path: "/infrastructure" }];
const InfrastructureMonitors = () => {
@@ -26,6 +27,7 @@ const InfrastructureMonitors = () => {
const dispatch = useDispatch();
// Local state
+ const [tab, setTab] = useState("hardware");
const [page, setPage] = useState(0);
const [updateTrigger, setUpdateTrigger] = useState(false);
const [selectedStatus, setSelectedStatus] = useState(undefined);
@@ -39,6 +41,14 @@ const InfrastructureMonitors = () => {
const { t } = useTranslation();
// Handlers
+ const handleTabChange = (event, newTab) => {
+ setTab(newTab);
+ setPage(0); // Reset to first page when changing tabs
+ setSearch(undefined); // Clear search
+ setSelectedStatus(undefined); // Clear filters
+ setToFilterStatus(undefined);
+ };
+
const handleActionMenuDelete = () => {
setUpdateTrigger(!updateTrigger);
};
@@ -70,9 +80,12 @@ const InfrastructureMonitors = () => {
const field = toFilterStatus !== undefined ? "status" : undefined;
+ // Determine monitor types based on active tab
+ const types = tab === "hardware" ? ["hardware"] : ["docker"];
+
const [monitors, summary, isLoading, networkError] = useFetchMonitorsByTeamId({
limit: 1,
- types: TYPES,
+ types: types,
page,
field: field,
filter: toFilterStatus ?? search,
@@ -82,55 +95,113 @@ const InfrastructureMonitors = () => {
return (
<>
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
>
);
};
diff --git a/server/src/db/v1/models/Check.js b/server/src/db/v1/models/Check.js
index 789cebaa5..98dcdcf50 100755
--- a/server/src/db/v1/models/Check.js
+++ b/server/src/db/v1/models/Check.js
@@ -54,6 +54,35 @@ const networkInterfaceSchema = mongoose.Schema({
fifo_out: { type: Number, default: 0 },
});
+const portSchema = mongoose.Schema({
+ port: { type: String },
+ protocol: { type: String },
+});
+
+const healthSchema = mongoose.Schema({
+ healthy: { type: Boolean, default: false },
+ source: { type: String, default: "" },
+ message: { type: String, default: "" },
+});
+
+const containerSchema = mongoose.Schema({
+ container_id: { type: String, default: "" },
+ container_name: { type: String, default: "" },
+ status: { type: String, default: "" },
+ health: {
+ type: healthSchema,
+ default: () => ({}),
+ },
+ running: { type: Boolean, default: false },
+ base_image: { type: String, default: "" },
+ exposed_ports: {
+ type: [portSchema],
+ default: () => [],
+ },
+ started_at: { type: Number, default: 0 },
+ finished_at: { type: Number, default: 0 },
+});
+
const CheckSchema = new mongoose.Schema(
{
// Common fields
@@ -148,6 +177,12 @@ const CheckSchema = new mongoose.Schema(
default: () => [],
},
+ // Docker fields
+ docker: {
+ type: [containerSchema],
+ default: () => [],
+ },
+
// PageSpeed fields
accessibility: {
type: Number,
diff --git a/server/src/service/v1/infrastructure/networkService.js b/server/src/service/v1/infrastructure/networkService.js
index d364f373e..9346737f0 100644
--- a/server/src/service/v1/infrastructure/networkService.js
+++ b/server/src/service/v1/infrastructure/networkService.js
@@ -286,7 +286,7 @@ class NetworkService {
}
}
- async requestDocker(monitor) {
+ async requestLocalDocker(monitor) {
try {
if (!monitor.url) {
throw new Error("Monitor URL is required");
@@ -330,7 +330,7 @@ class NetworkService {
this.logger.warn({
message: `No container found for "${monitor.url}".`,
service: this.SERVICE_NAME,
- method: "requestDocker",
+ method: "requestLocalDocker",
details: { url: monitor.url },
});
@@ -350,7 +350,7 @@ class NetworkService {
this.logger.warn({
message: `Ambiguous container match for "${monitor.url}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`,
service: this.SERVICE_NAME,
- method: "requestDocker",
+ method: "requestLocalDocker",
details: { url: monitor.url },
});
dockerResponse.status = 404;
@@ -375,6 +375,112 @@ class NetworkService {
}
return dockerResponse;
+ } catch (err) {
+ err.service = this.SERVICE_NAME;
+ err.method = "requestLocalDocker";
+ throw err;
+ }
+ }
+
+ async requestCaptureDocker(monitor) {
+ try {
+ if (!monitor.url) {
+ throw new Error("Monitor URL is required");
+ }
+
+ const dockerResponse = {
+ monitorId: monitor._id,
+ teamId: monitor.teamId,
+ type: monitor.type,
+ };
+
+ // Build Capture API URL
+ const captureUrl = new URL(monitor.url);
+ const apiUrl = `${captureUrl.protocol}//${captureUrl.host}/api/v1/metrics/docker`;
+
+ // Make request to Capture API
+ const { response, responseTime, error } = await this.timeRequest(async () => {
+ return await this.got(apiUrl, {
+ headers: {
+ Authorization: `Bearer ${monitor.secret || ""}`,
+ },
+ timeout: { request: 10000 },
+ responseType: "json",
+ });
+ });
+
+ dockerResponse.responseTime = responseTime;
+
+ // Handle errors
+ if (error) {
+ const statusCode = error.response?.statusCode || this.NETWORK_ERROR;
+ dockerResponse.status = false;
+ dockerResponse.code = statusCode;
+
+ if (statusCode === 401) {
+ dockerResponse.message = "Invalid API secret for Capture server";
+ } else if (statusCode === 404) {
+ dockerResponse.message = "Docker monitoring not available (Capture version too old or endpoint not found)";
+ } else if (statusCode === 500) {
+ dockerResponse.message = "Docker daemon unavailable on Capture server";
+ } else {
+ dockerResponse.message = error.message || "Failed to fetch Docker metrics from Capture";
+ }
+
+ return dockerResponse;
+ }
+
+ // Parse response
+ const payload = response.body || response;
+ const { data, capture, errors } = payload;
+
+ // Check for errors array
+ if (errors && errors.length > 0) {
+ // Check if it's a critical error (docker.client)
+ const dockerClientError = errors.find((e) => e.metric && e.metric.includes("docker.client"));
+ if (dockerClientError) {
+ dockerResponse.status = false;
+ dockerResponse.code = 500;
+ dockerResponse.message = dockerClientError.err || "Docker daemon unavailable";
+ return dockerResponse;
+ }
+ }
+
+ // Check if we have container data
+ if (!data || !Array.isArray(data) || data.length === 0) {
+ dockerResponse.status = false;
+ dockerResponse.code = 404;
+ dockerResponse.message = "No containers found on Capture server";
+ dockerResponse.payload = { data: [], capture, errors: errors || [] };
+ return dockerResponse;
+ }
+
+ // Successful response - store all containers
+ dockerResponse.status = true;
+ dockerResponse.code = 200;
+ dockerResponse.message = `Found ${data.length} container(s) on Capture server`;
+ dockerResponse.payload = { data, capture, errors: errors || [] };
+
+ return dockerResponse;
+ } catch (err) {
+ err.service = this.SERVICE_NAME;
+ err.method = "requestCaptureDocker";
+ throw err;
+ }
+ }
+
+ async requestDocker(monitor) {
+ try {
+ if (!monitor.url) {
+ throw new Error("Monitor URL is required");
+ }
+
+ // Check if URL is HTTP/HTTPS (Capture API) or container name/ID (local)
+ if (monitor.url.startsWith("http://") || monitor.url.startsWith("https://")) {
+ return await this.requestCaptureDocker(monitor);
+ } else {
+ return await this.requestLocalDocker(monitor);
+ }
} catch (err) {
err.service = this.SERVICE_NAME;
err.method = "requestDocker";
diff --git a/server/src/service/v1/infrastructure/statusService.js b/server/src/service/v1/infrastructure/statusService.js
index 248d1c910..851d5c210 100755
--- a/server/src/service/v1/infrastructure/statusService.js
+++ b/server/src/service/v1/infrastructure/statusService.js
@@ -275,6 +275,13 @@ class StatusService {
check.capture = payload?.capture ?? {};
check.net = net ?? {};
}
+
+ if (type === "docker") {
+ const { data, capture, errors } = payload ?? {};
+ check.docker = data ?? [];
+ check.errors = errors ?? [];
+ check.capture = capture ?? {};
+ }
return check;
};