Add Docker monitoring integration with Capture API (partial implementation)
Backend changes: - Update Check model to store Docker container data (container_id, status, health, ports, etc.) - Refactor networkService to support both local Docker and remote Capture API - requestLocalDocker: Existing local Docker socket monitoring - requestCaptureDocker: New Capture API endpoint integration - requestDocker: Routes based on URL pattern (http/https vs container name) - Update statusService to handle Docker check data storage - Enforce proper error handling for Capture API (401, 404, 500, 207 responses) Frontend changes: - Add Hardware/Docker tabs to Infrastructure page - Add query parameter support for monitor type selection - Prepare form for Docker monitor creation (partial) Status: Backend complete, frontend in progress Next: Complete Docker monitor form, add container table component, test end-to-end
This commit is contained in:
5 files changed
+278
-55
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<PageStateWrapper
|
||||
networkError={networkError}
|
||||
isLoading={isLoading}
|
||||
items={monitors}
|
||||
type="infrastructureMonitor"
|
||||
fallbackLink="/infrastructure/create"
|
||||
>
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<MonitorCreateHeader
|
||||
isAdmin={isAdmin}
|
||||
isLoading={isLoading}
|
||||
path="/infrastructure/create"
|
||||
/>
|
||||
<Stack direction={"row"}>
|
||||
<MonitorCountHeader
|
||||
isLoading={isLoading}
|
||||
monitorCount={summary?.totalMonitors ?? 0}
|
||||
/>
|
||||
<Filter
|
||||
selectedStatus={selectedStatus}
|
||||
setSelectedStatus={setSelectedStatus}
|
||||
setToFilterStatus={setToFilterStatus}
|
||||
handleReset={handleReset}
|
||||
/>
|
||||
<SearchComponent
|
||||
monitors={monitors}
|
||||
onSearchChange={setSearch}
|
||||
setIsSearching={setIsSearching}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<MonitorCreateHeader
|
||||
isAdmin={isAdmin}
|
||||
isLoading={isLoading}
|
||||
path={tab === "docker" ? "/infrastructure/create?type=docker" : "/infrastructure/create?type=hardware"}
|
||||
/>
|
||||
|
||||
<MonitorsTable
|
||||
isLoading={isLoading}
|
||||
monitors={monitors}
|
||||
isAdmin={isAdmin}
|
||||
handleActionMenuDelete={handleActionMenuDelete}
|
||||
isSearching={isSearching}
|
||||
/>
|
||||
<Pagination
|
||||
itemCount={summary?.totalMonitors}
|
||||
paginationLabel={t("monitors")}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangePage={handleChangePage}
|
||||
handleChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
/>
|
||||
</Stack>
|
||||
</PageStateWrapper>
|
||||
<TabContext value={tab}>
|
||||
<CustomTabList value={tab} onChange={handleTabChange}>
|
||||
<Tab label="Hardware" value="hardware" />
|
||||
<Tab label="Docker Monitoring" value="docker" />
|
||||
</CustomTabList>
|
||||
|
||||
<TabPanel value="hardware" sx={{ padding: 0 }}>
|
||||
<PageStateWrapper
|
||||
networkError={networkError}
|
||||
isLoading={isLoading}
|
||||
items={monitors}
|
||||
type="infrastructureMonitor"
|
||||
fallbackLink="/infrastructure/create"
|
||||
>
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Stack direction={"row"}>
|
||||
<MonitorCountHeader
|
||||
isLoading={isLoading}
|
||||
monitorCount={summary?.totalMonitors ?? 0}
|
||||
/>
|
||||
<Filter
|
||||
selectedStatus={selectedStatus}
|
||||
setSelectedStatus={setSelectedStatus}
|
||||
setToFilterStatus={setToFilterStatus}
|
||||
handleReset={handleReset}
|
||||
/>
|
||||
<SearchComponent
|
||||
monitors={monitors}
|
||||
onSearchChange={setSearch}
|
||||
setIsSearching={setIsSearching}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<MonitorsTable
|
||||
isLoading={isLoading}
|
||||
monitors={monitors}
|
||||
isAdmin={isAdmin}
|
||||
handleActionMenuDelete={handleActionMenuDelete}
|
||||
isSearching={isSearching}
|
||||
/>
|
||||
<Pagination
|
||||
itemCount={summary?.totalMonitors}
|
||||
paginationLabel={t("monitors")}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangePage={handleChangePage}
|
||||
handleChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
/>
|
||||
</Stack>
|
||||
</PageStateWrapper>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value="docker" sx={{ padding: 0 }}>
|
||||
<PageStateWrapper
|
||||
networkError={networkError}
|
||||
isLoading={isLoading}
|
||||
items={monitors}
|
||||
type="infrastructureMonitor"
|
||||
fallbackLink="/infrastructure/create"
|
||||
>
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Stack direction={"row"}>
|
||||
<MonitorCountHeader
|
||||
isLoading={isLoading}
|
||||
monitorCount={summary?.totalMonitors ?? 0}
|
||||
/>
|
||||
<Filter
|
||||
selectedStatus={selectedStatus}
|
||||
setSelectedStatus={setSelectedStatus}
|
||||
setToFilterStatus={setToFilterStatus}
|
||||
handleReset={handleReset}
|
||||
/>
|
||||
<SearchComponent
|
||||
monitors={monitors}
|
||||
onSearchChange={setSearch}
|
||||
setIsSearching={setIsSearching}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<MonitorsTable
|
||||
isLoading={isLoading}
|
||||
monitors={monitors}
|
||||
isAdmin={isAdmin}
|
||||
handleActionMenuDelete={handleActionMenuDelete}
|
||||
isSearching={isSearching}
|
||||
/>
|
||||
<Pagination
|
||||
itemCount={summary?.totalMonitors}
|
||||
paginationLabel={t("monitors")}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangePage={handleChangePage}
|
||||
handleChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
/>
|
||||
</Stack>
|
||||
</PageStateWrapper>
|
||||
</TabPanel>
|
||||
</TabContext>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user