Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4d2abd496 | ||
|
|
8f3edac281 | ||
|
|
fd9d265d23 | ||
|
|
0a90a30045 | ||
|
|
0f3c81c052 | ||
|
|
2d58facb97 |
No files matched your search
@@ -0,0 +1,993 @@
|
||||
# Checkmate Codebase Architecture - Docker Monitoring Integration Guide
|
||||
|
||||
## 1. PROJECT STRUCTURE
|
||||
|
||||
### Directory Layout
|
||||
```
|
||||
Checkmate/
|
||||
├── server/ # Backend Node.js/Express
|
||||
│ ├── src/
|
||||
│ │ ├── db/v1|v2/ # Database (MongoDB)
|
||||
│ │ ├── service/v1|v2/ # Business logic
|
||||
│ │ ├── routes/v1|v2/ # API routes
|
||||
│ │ ├── controllers/v1|v2/ # HTTP request handlers
|
||||
│ │ ├── middleware/v1/ # Express middleware
|
||||
│ │ ├── config/ # Service initialization
|
||||
│ │ ├── validation/ # Input validation
|
||||
│ │ └── utils/ # Helper functions
|
||||
│ └── package.json # Dependencies (Node >=20)
|
||||
├── client/ # Frontend React + TypeScript
|
||||
│ ├── src/
|
||||
│ │ ├── Components/v1|v2/ # UI Components
|
||||
│ │ ├── Pages/v1|v2/ # Page layouts
|
||||
│ │ ├── Hooks/ # React hooks (data fetching)
|
||||
│ │ ├── Features/ # Redux slices
|
||||
│ │ └── Utils/ # Utilities & API client
|
||||
│ └── package.json # React, Vite, MUI
|
||||
└── docker/ # Deployment configs
|
||||
```
|
||||
|
||||
### Technology Stack
|
||||
**Backend:**
|
||||
- Express 4.19 (HTTP server)
|
||||
- MongoDB 8.3 (database)
|
||||
- Mongoose (ODM)
|
||||
- BullMQ 5.41 (job queue) - V2 only
|
||||
- super-simple-scheduler 1.4 (job scheduling) - V1
|
||||
- Dockerode 4.0.6 (Docker API client)
|
||||
- JWT authentication
|
||||
- Winston (logging)
|
||||
|
||||
**Frontend:**
|
||||
- React 18.3 + TypeScript
|
||||
- Vite (build tool)
|
||||
- MUI v6 (Material-UI)
|
||||
- Redux Toolkit (state management)
|
||||
- Axios (HTTP client)
|
||||
- React Router (navigation)
|
||||
- i18next (internationalization)
|
||||
|
||||
**Architectures:**
|
||||
- V1: Simpler, SuperSimpleQueue-based job scheduling
|
||||
- V2: TypeScript-based, BullMQ, more structured (in development)
|
||||
|
||||
---
|
||||
|
||||
## 2. MONITOR TYPES & IMPLEMENTATION
|
||||
|
||||
### Supported Monitor Types
|
||||
Defined in `/server/src/db/v1/models/Monitor.js` line 46:
|
||||
```javascript
|
||||
type: {
|
||||
enum: ["http", "ping", "pagespeed", "hardware", "docker", "port", "game"],
|
||||
required: true
|
||||
}
|
||||
```
|
||||
|
||||
### How Monitor Types Are Handled
|
||||
|
||||
#### Request Dispatch (NetworkService)
|
||||
**Location:** `/server/src/service/v1/infrastructure/networkService.js` (580 lines)
|
||||
|
||||
Entry point: `requestStatus(monitor)` - Routes to type-specific handlers:
|
||||
```javascript
|
||||
async requestStatus(monitor) {
|
||||
switch(monitor.type) {
|
||||
case "ping": return await this.requestPing(monitor);
|
||||
case "http": return await this.requestHttp(monitor);
|
||||
case "pagespeed": return await this.requestPageSpeed(monitor);
|
||||
case "hardware": return await this.requestHardware(monitor);
|
||||
case "docker": return await this.requestDocker(monitor);
|
||||
case "port": return await this.requestPort(monitor);
|
||||
case "game": return await this.requestGame(monitor);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### DOCKER IMPLEMENTATION (Lines 289-383)
|
||||
```javascript
|
||||
async requestDocker(monitor) {
|
||||
// Uses Dockerode to connect to /var/run/docker.sock
|
||||
const docker = new this.Docker({
|
||||
socketPath: "/var/run/docker.sock",
|
||||
handleError: true,
|
||||
});
|
||||
|
||||
// Container matching (priority-based):
|
||||
// 1. Exact full ID match (64-char)
|
||||
// 2. Exact name match (case-insensitive)
|
||||
// 3. Partial ID match (backwards compatibility)
|
||||
|
||||
// Returns: { status: bool, code: int, message: string, responseTime: ms }
|
||||
}
|
||||
```
|
||||
|
||||
**Docker Monitor Storage:**
|
||||
- `monitor.url`: Container ID or name (e.g., "my-app" or "a1b2c3d4...")
|
||||
- `monitor.port`: Not used for Docker
|
||||
- Returns: Only status and response time (no detailed metrics)
|
||||
|
||||
#### HARDWARE IMPLEMENTATION (Lines 279-287)
|
||||
```javascript
|
||||
async requestHardware(monitor) {
|
||||
return await this.requestHttp(monitor); // Delegates to HTTP!
|
||||
}
|
||||
```
|
||||
Hardware monitors send HTTP requests to Capture API endpoint with secret authentication.
|
||||
|
||||
### Hardware/Docker Check Data Structure
|
||||
**Location:** `/server/src/db/v1/models/Check.js` (176 lines)
|
||||
|
||||
Check schema includes:
|
||||
```javascript
|
||||
// Common fields
|
||||
monitorId, teamId, type, status, responseTime, message
|
||||
|
||||
// Hardware/Docker specific (capture data):
|
||||
cpu: {
|
||||
physical_core, logical_core, frequency, temperature[],
|
||||
free_percent, usage_percent
|
||||
},
|
||||
memory: {
|
||||
total_bytes, available_bytes, used_bytes, usage_percent
|
||||
},
|
||||
disk: [{
|
||||
read_speed_bytes, write_speed_bytes,
|
||||
total_bytes, free_bytes, usage_percent
|
||||
}],
|
||||
host: { os, platform, kernel_version },
|
||||
errors: [{ metric[], err }],
|
||||
capture: { version, mode },
|
||||
net: [{ name, bytes_sent, bytes_recv, ... }]
|
||||
```
|
||||
|
||||
**TTL:** Auto-deletes after 30 days
|
||||
**Indexes:** monitorId, type, status, updatedAt (optimized for queries)
|
||||
|
||||
---
|
||||
|
||||
## 3. DATABASE SCHEMA
|
||||
|
||||
### Core Models
|
||||
|
||||
#### Monitor Model
|
||||
**File:** `/server/src/db/v1/models/Monitor.js` (208 lines)
|
||||
|
||||
```javascript
|
||||
{
|
||||
userId: ObjectId (immutable, required),
|
||||
teamId: ObjectId (immutable, required),
|
||||
name: String (required),
|
||||
description: String,
|
||||
type: String (enum: http|ping|pagespeed|hardware|docker|port|game),
|
||||
url: String (required) // container name/ID for docker
|
||||
port: Number,
|
||||
interval: Number (ms, default: 60000),
|
||||
isActive: Boolean (default: true),
|
||||
status: Boolean, // Current up/down
|
||||
statusWindow: [Boolean], // Last N checks
|
||||
statusWindowSize: Number (default: 5),
|
||||
statusWindowThreshold: Number (default: 60),
|
||||
|
||||
// Hardware/Docker thresholds
|
||||
thresholds: {
|
||||
usage_cpu: Number (0-1),
|
||||
usage_memory: Number (0-1),
|
||||
usage_disk: Number (0-1),
|
||||
usage_temperature: Number
|
||||
},
|
||||
|
||||
// Notification thresholds
|
||||
alertThreshold: Number (default: 5),
|
||||
cpuAlertThreshold, memoryAlertThreshold, diskAlertThreshold, tempAlertThreshold,
|
||||
|
||||
// Relationships
|
||||
notifications: [ObjectId], // refs to Notification model
|
||||
|
||||
// Advanced
|
||||
secret: String, // For Capture API auth
|
||||
ignoreTlsErrors: Boolean,
|
||||
jsonPath: String,
|
||||
expectedValue: String,
|
||||
matchMethod: String (equal|include|regex),
|
||||
gameId: String,
|
||||
group: String (max 50 chars),
|
||||
|
||||
// Computed
|
||||
uptimePercentage: Number,
|
||||
|
||||
timestamps: true // createdAt, updatedAt
|
||||
}
|
||||
|
||||
// Indexes
|
||||
MonitorSchema.index({ teamId: 1, type: 1 });
|
||||
|
||||
// Pre-hooks
|
||||
pre("findOneAndDelete"): Deletes checks, stats, removes from status pages
|
||||
pre("deleteMany"): Cascade deletes
|
||||
pre("save"): Syncs alertThresholds
|
||||
pre("findOneAndUpdate"): Updates thresholds
|
||||
```
|
||||
|
||||
#### Check Model
|
||||
**File:** `/server/src/db/v1/models/Check.js`
|
||||
|
||||
```javascript
|
||||
{
|
||||
monitorId: ObjectId (ref: Monitor, immutable, indexed),
|
||||
teamId: ObjectId (ref: Team, immutable, indexed),
|
||||
type: String (enum: http|ping|pagespeed|hardware|docker|port|game, indexed),
|
||||
status: Boolean (indexed),
|
||||
responseTime: Number,
|
||||
timings: Object,
|
||||
statusCode: Number,
|
||||
message: String,
|
||||
|
||||
// Acknowledgment
|
||||
ack: Boolean (default: false),
|
||||
ackAt: Date,
|
||||
|
||||
// Hardware fields (see above)
|
||||
cpu, memory, disk[], host, errors[], capture, net[]
|
||||
|
||||
// PageSpeed only
|
||||
accessibility, bestPractices, seo, performance, audits,
|
||||
|
||||
// Expiry
|
||||
expiry: Date (TTL index: 30 days),
|
||||
timestamps: true
|
||||
}
|
||||
|
||||
// Indexes
|
||||
CheckSchema.index({ updatedAt: 1 });
|
||||
CheckSchema.index({ monitorId: 1, updatedAt: 1 });
|
||||
CheckSchema.index({ monitorId: 1, updatedAt: -1 });
|
||||
CheckSchema.index({ teamId: 1, updatedAt: -1 });
|
||||
```
|
||||
|
||||
#### MonitorStats Model
|
||||
**File:** `/server/src/db/v1/models/MonitorStats.js`
|
||||
|
||||
```javascript
|
||||
{
|
||||
monitorId: ObjectId (ref: Monitor, immutable, indexed),
|
||||
avgResponseTime: Number,
|
||||
totalChecks: Number,
|
||||
totalUpChecks: Number,
|
||||
totalDownChecks: Number,
|
||||
uptimePercentage: Number,
|
||||
lastCheckTimestamp: Number,
|
||||
lastResponseTime: Number,
|
||||
timeOfLastFailure: Number,
|
||||
timestamps: true
|
||||
}
|
||||
```
|
||||
|
||||
### Database Access Layer
|
||||
**Location:** `/server/src/db/v1/modules/`
|
||||
|
||||
- `MonitorModule.js` (593 lines) - Monitor CRUD + aggregations
|
||||
- `CheckModule.js` - Check recording
|
||||
- `MonitorModuleQueries.js` - MongoDB aggregation pipelines
|
||||
|
||||
**Key Methods:**
|
||||
```javascript
|
||||
// Creation
|
||||
Monitor.createMonitor({ body, teamId, userId })
|
||||
Monitor.createBulkMonitors(monitors)
|
||||
|
||||
// Retrieval
|
||||
Monitor.getMonitorById(id)
|
||||
Monitor.getMonitorsByTeamId()
|
||||
Monitor.getMonitorStatsById()
|
||||
Monitor.getHardwareDetailsById()
|
||||
|
||||
// Updates
|
||||
Monitor.updateMonitor(id, updateData)
|
||||
|
||||
// Deletions
|
||||
Monitor.deleteMonitor(id)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. API PATTERNS
|
||||
|
||||
### REST Endpoint Structure
|
||||
**Base:** `/api/v1/`
|
||||
|
||||
#### Monitor Routes
|
||||
**File:** `/server/src/routes/v1/monitorRoute.js`
|
||||
|
||||
```
|
||||
GET /monitors getAllMonitors
|
||||
POST /monitors createMonitor
|
||||
DELETE /monitors deleteAllMonitors (superadmin)
|
||||
|
||||
GET /monitors/:monitorId getMonitorById
|
||||
PUT /monitors/:monitorId editMonitor (admin)
|
||||
DELETE /monitors/:monitorId deleteMonitor (admin)
|
||||
|
||||
GET /monitors/team getMonitorsByTeamId
|
||||
GET /monitors/team/with-checks getMonitorsWithChecksByTeamId
|
||||
GET /monitors/team/summary getMonitorsSummaryByTeamId
|
||||
GET /monitors/team/groups getGroupsByTeamId
|
||||
|
||||
GET /monitors/stats/:monitorId getMonitorStatsById
|
||||
GET /monitors/uptime/details/:monitorId getUptimeDetailsById
|
||||
GET /monitors/hardware/details/:monitorId getHardwareDetailsById
|
||||
|
||||
POST /monitors/pause/:monitorId pauseMonitor (admin)
|
||||
POST /monitors/demo addDemoMonitors
|
||||
POST /monitors/bulk createBulkMonitors (CSV upload)
|
||||
POST /monitors/test-email sendTestEmail
|
||||
|
||||
GET /monitors/games getAllGames
|
||||
GET /monitors/certificate/:monitorId fetchCertificate
|
||||
```
|
||||
|
||||
#### Check Routes
|
||||
**File:** `/server/src/routes/v1/checkRoute.js`
|
||||
|
||||
```
|
||||
GET /checks/:monitorId getChecksByMonitorId
|
||||
GET /checks/team/:teamId getChecksByTeamId
|
||||
GET /checks/:monitorId/:checkId getCheckById
|
||||
PUT /checks/:checkId updateCheck (ack)
|
||||
```
|
||||
|
||||
### Request/Response Pattern
|
||||
|
||||
**Create Monitor Request:**
|
||||
```javascript
|
||||
POST /api/v1/monitors
|
||||
Authorization: Bearer <JWT>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
name: "My Docker Container",
|
||||
description: "Monitors app container",
|
||||
type: "docker",
|
||||
url: "my-app-container", // Container name
|
||||
interval: 60000, // 1 minute
|
||||
isActive: true,
|
||||
secret: "api-key-for-capture", // If using hardware monitoring
|
||||
notifications: ["notif-id-1"],
|
||||
thresholds: {
|
||||
usage_cpu: 0.8, // 80%
|
||||
usage_memory: 0.9, // 90%
|
||||
usage_disk: 0.95
|
||||
},
|
||||
alertThreshold: 5, // Consecutive failures
|
||||
statusWindowSize: 5, // Last 5 checks
|
||||
statusWindowThreshold: 60 // % threshold
|
||||
}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
data: {
|
||||
_id: "monitor-id",
|
||||
name: "My Docker Container",
|
||||
type: "docker",
|
||||
status: true,
|
||||
interval: 60000,
|
||||
...
|
||||
},
|
||||
msg: "Monitor created successfully"
|
||||
}
|
||||
```
|
||||
|
||||
**Edit Monitor Request:**
|
||||
```javascript
|
||||
PUT /api/v1/monitors/:monitorId
|
||||
|
||||
{
|
||||
name: "Updated Name",
|
||||
interval: 120000,
|
||||
thresholds: { usage_cpu: 0.85 },
|
||||
// Partial updates allowed
|
||||
}
|
||||
```
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
**JWT Middleware:** `/server/src/middleware/v1/verifyJWT.js`
|
||||
- Header: `Authorization: Bearer <token>`
|
||||
- Validates signature using `jwtSecret` from AppSettings
|
||||
- Extracts user info: `req.user = { userId, teamId, role, ... }`
|
||||
|
||||
**Role-Based Access:**
|
||||
```javascript
|
||||
// isAllowed middleware checks req.user.role
|
||||
router.post("/monitors", isAllowed(["admin", "superadmin"]), createMonitor);
|
||||
router.delete("/monitors/:monitorId", isAllowed(["admin", "superadmin"]), deleteMonitor);
|
||||
```
|
||||
|
||||
### Validation
|
||||
**File:** `/server/src/validation/joi.js`
|
||||
|
||||
```javascript
|
||||
const createMonitorBodyValidation = joi.object({
|
||||
name: joi.string().required(),
|
||||
description: joi.string().required(),
|
||||
type: joi.string().required(),
|
||||
url: joi.string().required(),
|
||||
interval: joi.number(),
|
||||
port: joi.number(),
|
||||
thresholds: joi.object().keys({
|
||||
usage_cpu: joi.number(),
|
||||
usage_memory: joi.number(),
|
||||
usage_disk: joi.number(),
|
||||
usage_temperature: joi.number(),
|
||||
}),
|
||||
notifications: joi.array().items(joi.string()),
|
||||
secret: joi.string(),
|
||||
// ... more fields
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. POLLING/JOBS IMPLEMENTATION
|
||||
|
||||
### Job Queue Architecture
|
||||
|
||||
#### V1: SuperSimpleScheduler (Production)
|
||||
**Files:**
|
||||
- `/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js`
|
||||
- `/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js`
|
||||
|
||||
**Job Lifecycle:**
|
||||
|
||||
1. **Initialization** (on server startup):
|
||||
```javascript
|
||||
// Load all monitors from DB
|
||||
const monitors = await db.monitorModule.getAllMonitors();
|
||||
for (const monitor of monitors) {
|
||||
// Add job with random offset (0-100ms)
|
||||
this.addJob(monitor._id, monitor);
|
||||
}
|
||||
|
||||
// Register job template
|
||||
scheduler.addTemplate("monitor-job", this.helper.getMonitorJob());
|
||||
```
|
||||
|
||||
2. **Job Template** (executed per interval):
|
||||
```javascript
|
||||
getMonitorJob = () => {
|
||||
return async (monitor) => {
|
||||
// Check maintenance window
|
||||
if (await this.isInMaintenanceWindow(monitorId, teamId)) {
|
||||
return; // Skip
|
||||
}
|
||||
|
||||
// Execute monitor check
|
||||
const networkResponse = await this.networkService.requestStatus(monitor);
|
||||
|
||||
// Update monitor status
|
||||
const { monitor: updated, statusChanged } =
|
||||
await this.statusService.updateStatus(networkResponse);
|
||||
|
||||
// Send notifications if status changed
|
||||
if (statusChanged) {
|
||||
await this.notificationService.handleNotifications({
|
||||
...networkResponse,
|
||||
monitor: updated,
|
||||
prevStatus,
|
||||
statusChanged
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
3. **Job Control:**
|
||||
```javascript
|
||||
// Add job
|
||||
addJob(monitorId, monitor) {
|
||||
scheduler.addJob({
|
||||
id: monitorId.toString(),
|
||||
template: "monitor-job",
|
||||
repeat: monitor.interval, // Milliseconds
|
||||
active: monitor.isActive,
|
||||
data: monitor.toObject()
|
||||
});
|
||||
}
|
||||
|
||||
// Pause/Resume
|
||||
pauseJob(monitor) { scheduler.pauseJob(monitor._id); }
|
||||
resumeJob(monitor) { scheduler.resumeJob(monitor._id); }
|
||||
|
||||
// Update interval
|
||||
updateJob(monitor) {
|
||||
scheduler.updateJob(monitor._id, {
|
||||
repeat: monitor.interval,
|
||||
data: monitor.toObject()
|
||||
});
|
||||
}
|
||||
|
||||
// Delete
|
||||
deleteJob(monitor) { scheduler.removeJob(monitor._id); }
|
||||
```
|
||||
|
||||
#### V2: BullMQ (TypeScript, Planned)
|
||||
**Files:**
|
||||
- `/server/src/service/v2/infrastructure/JobQueue.ts`
|
||||
- `/server/src/service/v2/infrastructure/JobGenerator.ts`
|
||||
|
||||
**Key Differences:**
|
||||
- Uses Redis backing store
|
||||
- TypeScript interfaces (type safety)
|
||||
- More robust queue management
|
||||
- Cleanup jobs for orphaned checks
|
||||
|
||||
```typescript
|
||||
// Cleanup job runs every 24 hours
|
||||
scheduler.addJob({
|
||||
id: "cleanup-orphaned-checks",
|
||||
template: "cleanup-job",
|
||||
repeat: 24 * 60 * 60 * 1000,
|
||||
active: true
|
||||
});
|
||||
```
|
||||
|
||||
### Monitor Execution Flow
|
||||
|
||||
```
|
||||
Monitor Created/Updated
|
||||
↓
|
||||
MonitorService.createMonitor()
|
||||
↓
|
||||
jobQueue.addJob(monitor)
|
||||
↓
|
||||
Job scheduled at interval
|
||||
↓
|
||||
[Job Execution]
|
||||
├─ Check maintenance window
|
||||
├─ NetworkService.requestStatus(monitor)
|
||||
│ └─ Routes to type handler (requestDocker, requestHttp, etc)
|
||||
├─ Create Check record (database)
|
||||
├─ StatusService.updateMonitorStatus()
|
||||
│ └─ Updates Monitor.status field
|
||||
├─ MonitorStatsService.updateStats()
|
||||
├─ If status changed → NotificationService.handleNotifications()
|
||||
└─ Job completes / reschedules for next interval
|
||||
```
|
||||
|
||||
### Status Update Logic
|
||||
**Location:** `/server/src/service/v1/infrastructure/statusService.js`
|
||||
|
||||
```javascript
|
||||
async updateStatus(networkResponse) {
|
||||
// networkResponse = { monitorId, type, status, code, message, responseTime }
|
||||
|
||||
// 1. Create Check record
|
||||
const check = await Check.create({
|
||||
monitorId, teamId, type, status,
|
||||
responseTime, statusCode: code, message
|
||||
});
|
||||
|
||||
// 2. Get previous status
|
||||
const prevStatus = monitor.status;
|
||||
|
||||
// 3. Apply statusWindow logic (sliding window of checks)
|
||||
monitor.statusWindow.push(status);
|
||||
if (monitor.statusWindow.length > monitor.statusWindowSize) {
|
||||
monitor.statusWindow.shift();
|
||||
}
|
||||
|
||||
// 4. Determine if DOWN (majority of window is false)
|
||||
const downCount = monitor.statusWindow.filter(s => !s).length;
|
||||
const isDown = (downCount / monitor.statusWindowSize) >
|
||||
(monitor.statusWindowThreshold / 100);
|
||||
|
||||
monitor.status = isDown ? false : true;
|
||||
|
||||
// 5. Return status change info
|
||||
return {
|
||||
monitor: await monitor.save(),
|
||||
statusChanged: prevStatus !== monitor.status,
|
||||
prevStatus
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. FRONTEND COMPONENTS
|
||||
|
||||
### Monitor Management Pages
|
||||
|
||||
#### Infrastructure Monitors (Hardware/Docker)
|
||||
**Location:** `/client/src/Pages/v1/Infrastructure/`
|
||||
|
||||
```
|
||||
Infrastructure/
|
||||
├── Create/
|
||||
│ ├── index.jsx (Main form - 10K lines)
|
||||
│ ├── Components/
|
||||
│ │ ├── MonitorStatusHeader.jsx
|
||||
│ │ ├── MonitorActionButtons.jsx
|
||||
│ │ └── CustomAlertsSection.jsx
|
||||
│ └── hooks/
|
||||
│ ├── useInfrastructureMonitorForm.jsx
|
||||
│ ├── useValidateInfrastructureForm.jsx
|
||||
│ └── useInfrastructureSubmit.jsx
|
||||
├── Details/
|
||||
├── Monitors/
|
||||
│ └── Components/
|
||||
│ └── MonitorsTable.jsx
|
||||
```
|
||||
|
||||
#### Form Structure (useInfrastructureMonitorForm hook)
|
||||
```javascript
|
||||
const [infrastructureMonitor, setInfrastructureMonitor] = useState({
|
||||
url: "", // Container name/IP
|
||||
name: "", // Display name
|
||||
notifications: [], // Notification IDs
|
||||
interval: 0.25, // Minutes (0.25 = 15s, 1 = 60s)
|
||||
|
||||
// Threshold toggles
|
||||
cpu: false,
|
||||
usage_cpu: "", // Percentage
|
||||
memory: false,
|
||||
usage_memory: "",
|
||||
disk: false,
|
||||
usage_disk: "",
|
||||
temperature: false,
|
||||
usage_temperature: "",
|
||||
|
||||
secret: "", // Capture API secret
|
||||
statusWindowSize: 5,
|
||||
statusWindowThreshold: 60
|
||||
});
|
||||
```
|
||||
|
||||
**Frequency Options:**
|
||||
```javascript
|
||||
[
|
||||
{ _id: 0.25, name: "15 Seconds" },
|
||||
{ _id: 0.5, name: "30 Seconds" },
|
||||
{ _id: 1, name: "1 Minute" },
|
||||
{ _id: 2, name: "2 Minutes" },
|
||||
{ _id: 5, name: "5 Minutes" },
|
||||
{ _id: 10, name: "10 Minutes" }
|
||||
]
|
||||
```
|
||||
|
||||
#### Form Submission (useInfrastructureSubmit hook)
|
||||
```javascript
|
||||
// Converts frontend form → backend model
|
||||
const finalForm = {
|
||||
url: `http${https ? "s" : ""}://` + infrastructureMonitor.url,
|
||||
name: infrastructureMonitor.name || infrastructureMonitor.url,
|
||||
interval: infrastructureMonitor.interval * 60000, // Convert to ms
|
||||
type: "hardware", // Always "hardware" for infrastructure monitors
|
||||
|
||||
// Thresholds (convert % to decimal 0-1)
|
||||
thresholds: {
|
||||
...(cpu ? { usage_cpu: usage_cpu / 100 } : {}),
|
||||
...(memory ? { usage_memory: usage_memory / 100 } : {}),
|
||||
...(disk ? { usage_disk: usage_disk / 100 } : {}),
|
||||
...(temperature ? { usage_temperature: usage_temperature / 100 } : {})
|
||||
},
|
||||
|
||||
notifications: infrastructureMonitor.notifications
|
||||
};
|
||||
|
||||
// API call
|
||||
isCreate ?
|
||||
await createMonitor({ monitor: finalForm, redirect: "/infrastructure" }) :
|
||||
await updateMonitor({ monitor: finalForm, redirect: "/infrastructure" });
|
||||
```
|
||||
|
||||
### Monitor Display Components
|
||||
|
||||
#### Monitor Status Component
|
||||
**Location:** `/client/src/Components/v2/Monitors/MonitorStatus.tsx`
|
||||
|
||||
Displays:
|
||||
- Status indicator (up/down)
|
||||
- Response time
|
||||
- Last check time
|
||||
- Incident badges
|
||||
|
||||
#### Monitoring Hooks
|
||||
**Location:** `/client/src/Hooks/v1/monitorHooks.js`
|
||||
|
||||
```javascript
|
||||
// Fetch monitors
|
||||
useFetchMonitorsByTeamId({ types, limit, page, ... })
|
||||
useFetchMonitorsWithSummary({ types, monitorUpdateTrigger })
|
||||
useFetchMonitorsWithChecks({ types, limit, ... })
|
||||
|
||||
// Fetch individual monitor
|
||||
useFetchHardwareMonitorById({ monitorId, updateTrigger })
|
||||
useFetchStatsByMonitorId({ monitorId, dateRange, ... })
|
||||
|
||||
// Manage monitors
|
||||
useCreateMonitor() // POST /monitors
|
||||
useUpdateMonitor() // PUT /monitors/:id
|
||||
useDeleteMonitor() // DELETE /monitors/:id
|
||||
usePauseMonitor() // POST /monitors/pause/:id
|
||||
```
|
||||
|
||||
### Network Service (API Client)
|
||||
**Location:** `/client/src/Utils/NetworkService.js` (1157 lines)
|
||||
|
||||
```javascript
|
||||
// Constructor
|
||||
constructor(store, dispatch, navigate) {
|
||||
// Axios instance with:
|
||||
// - Bearer token injection
|
||||
// - Language header
|
||||
// - 401 redirect to login
|
||||
// - Network error handling
|
||||
}
|
||||
|
||||
// Monitor endpoints
|
||||
async createMonitor(config) {
|
||||
return this.axiosInstance.post(`/monitors`, config.monitor);
|
||||
}
|
||||
|
||||
async updateMonitor(config) {
|
||||
return this.axiosInstance.put(`/monitors/${config.monitorId}`, config.monitor);
|
||||
}
|
||||
|
||||
async getMonitorsByTeamId(config) {
|
||||
// Query params: limit, types[], page, rowsPerPage, filter, field, order
|
||||
return this.axiosInstance.get(`/monitors/team?...`);
|
||||
}
|
||||
|
||||
async getHardwareDetailsByMonitorId(config) {
|
||||
return this.axiosInstance.get(`/monitors/hardware/details/${config.monitorId}`);
|
||||
}
|
||||
|
||||
async getStatsByMonitorId(config) {
|
||||
// Query params: sortOrder, limit, dateRange, numToDisplay, normalize
|
||||
return this.axiosInstance.get(`/monitors/stats/${config.monitorId}?...`);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. AUTHENTICATION & API KEYS
|
||||
|
||||
### User Authentication (JWT)
|
||||
|
||||
**Registration/Login:**
|
||||
```javascript
|
||||
// POST /auth/register
|
||||
{
|
||||
firstName: "John",
|
||||
lastName: "Doe",
|
||||
email: "john@example.com",
|
||||
password: "SecurePass123!"
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
user: { _id, email, role, teamId, ... },
|
||||
token: "eyJhbGciOiJIUzI1NiIs..." // JWT
|
||||
}
|
||||
```
|
||||
|
||||
**Token Storage:** Redux store (`Features/Auth/authSlice`)
|
||||
|
||||
**Token Injection:** Automatic by NetworkService
|
||||
```javascript
|
||||
config.headers.Authorization = `Bearer ${authToken}`;
|
||||
```
|
||||
|
||||
### Monitor-Specific Authentication
|
||||
|
||||
#### For Hardware/Docker Monitors via Capture API
|
||||
Monitors can have a `secret` field for authenticating requests to the Capture API:
|
||||
|
||||
```javascript
|
||||
{
|
||||
type: "hardware",
|
||||
url: "http://192.168.1.100:3000", // Capture endpoint
|
||||
secret: "api-key-12345", // Passed to Capture API
|
||||
thresholds: { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation:** The secret is stored in the Monitor model and could be:
|
||||
1. Sent as HTTP header: `Authorization: Bearer <secret>`
|
||||
2. Sent as query param: `?secret=<secret>`
|
||||
3. Sent in request body for POST requests
|
||||
|
||||
---
|
||||
|
||||
## 8. CONFIGURATION & USER SETTINGS
|
||||
|
||||
### Global Settings
|
||||
**Model:** `/server/src/db/v1/models/AppSettings.js`
|
||||
|
||||
```javascript
|
||||
{
|
||||
jwtSecret: String, // For signing JWTs
|
||||
sslCertificate: String, // SSL cert content
|
||||
smtpConfig: Object, // Email settings
|
||||
globalThresholds: { // Default thresholds
|
||||
cpu: Number,
|
||||
memory: Number,
|
||||
disk: Number,
|
||||
temperature: Number
|
||||
},
|
||||
siteTitle: String,
|
||||
defaultLanguage: String,
|
||||
maintenanceMode: Boolean
|
||||
}
|
||||
```
|
||||
|
||||
**Fetching Settings:**
|
||||
```javascript
|
||||
// Frontend
|
||||
useFetchGlobalSettings()
|
||||
→ NetworkService.getGlobalSettings()
|
||||
→ GET /settings
|
||||
|
||||
// Backend
|
||||
SettingsService.loadSettings()
|
||||
→ Loads from AppSettings collection
|
||||
```
|
||||
|
||||
### Monitor Configuration UI Flow
|
||||
|
||||
1. **Create Page:** `/infrastructure/create`
|
||||
- Fetch global settings for default thresholds
|
||||
- Form includes:
|
||||
- Monitor name
|
||||
- URL/Container name
|
||||
- Interval (dropdown)
|
||||
- CPU/Memory/Disk/Temp thresholds (checkboxes + inputs)
|
||||
- Notification channels (multi-select)
|
||||
- Secret (if using Capture API)
|
||||
|
||||
2. **Edit Page:** `/infrastructure/configure/:monitorId`
|
||||
- Load existing monitor data
|
||||
- Update form fields
|
||||
- Save changes with PUT request
|
||||
|
||||
3. **Details Page:** `/infrastructure/:monitorId`
|
||||
- Display monitor stats
|
||||
- Show uptime graph
|
||||
- List recent checks
|
||||
- Status history
|
||||
|
||||
### Team-Based Isolation
|
||||
**Access Control:**
|
||||
```javascript
|
||||
// All queries filtered by teamId
|
||||
await Monitor.find({ teamId: req.user.teamId });
|
||||
|
||||
// verifyTeamAccess middleware
|
||||
await verifyTeamAccess(req.user.teamId, req.params.monitorId);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## KEY PATTERNS FOR DOCKER MONITORING INTEGRATION
|
||||
|
||||
### 1. Adding Docker Monitor Type
|
||||
Already supported! Located in:
|
||||
- `Monitor.schema` type enum ✓
|
||||
- `NetworkService.requestDocker()` ✓
|
||||
- `Check.schema` with type ✓
|
||||
|
||||
### 2. Docker-Specific Data Collection
|
||||
Currently: Only status + response time
|
||||
**To Add Detailed Metrics:**
|
||||
- Extend `Check.schema` with Docker stats fields
|
||||
- Modify `NetworkService.requestDocker()` to call `container.stats()`
|
||||
- Stream stats to `Check.docker` field
|
||||
|
||||
### 3. Capture API Integration (Hardware Monitoring)
|
||||
**Flow:**
|
||||
```
|
||||
Monitor with type="hardware" and url="http://capture-endpoint"
|
||||
↓
|
||||
NetworkService.requestHardware()
|
||||
↓ (delegates to)
|
||||
NetworkService.requestHttp()
|
||||
↓
|
||||
Expects JSON response: { cpu, memory, disk, net, ... }
|
||||
↓
|
||||
Check record stores full payload in hardware fields
|
||||
```
|
||||
|
||||
### 4. Database Queries
|
||||
**Get hardware checks:**
|
||||
```javascript
|
||||
Check.find({
|
||||
monitorId: id,
|
||||
type: "hardware",
|
||||
createdAt: { $gte: startDate, $lte: endDate }
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(100)
|
||||
```
|
||||
|
||||
### 5. UI Display
|
||||
- Stats dashboard: `/infrastructure/:monitorId`
|
||||
- Uptime details: `/monitor/uptime/details/:monitorId`
|
||||
- Hardware details: `/monitor/hardware/details/:monitorId`
|
||||
|
||||
---
|
||||
|
||||
## SERVICE ARCHITECTURE
|
||||
|
||||
### Service Registry Pattern
|
||||
**Location:** `/server/src/service/v1/system/serviceRegistry.js`
|
||||
|
||||
```javascript
|
||||
// Singleton pattern
|
||||
ServiceRegistry.set(serviceName, serviceInstance);
|
||||
const service = ServiceRegistry.get(serviceName);
|
||||
```
|
||||
|
||||
### Service Dependencies
|
||||
**Initialization:** `/server/src/config/services.js`
|
||||
|
||||
```javascript
|
||||
// Infrastructure services
|
||||
new NetworkService({
|
||||
axios, got, https, jmespath, GameDig, ping,
|
||||
logger, http, Docker, // ← Docker client injected
|
||||
net, stringService, settingsService
|
||||
})
|
||||
|
||||
// Business services
|
||||
new MonitorService({
|
||||
db, settingsService, jobQueue,
|
||||
stringService, emailService, logger
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## KEY FILES SUMMARY
|
||||
|
||||
| Component | File | Lines | Purpose |
|
||||
|-----------|------|-------|---------|
|
||||
| Monitor Model | `server/src/db/v1/models/Monitor.js` | 209 | Schema, hooks |
|
||||
| Check Model | `server/src/db/v1/models/Check.js` | 177 | Check records, TTL |
|
||||
| NetworkService | `server/src/service/v1/infrastructure/networkService.js` | 580 | Type handlers |
|
||||
| MonitorService | `server/src/service/v1/business/monitorService.js` | 277 | CRUD, validation |
|
||||
| MonitorModule | `server/src/db/v1/modules/monitorModule.js` | 593 | DB queries |
|
||||
| JobQueue | `server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js` | 177 | Job scheduling |
|
||||
| MonitorController | `server/src/controllers/v1/monitorController.js` | 475 | HTTP handlers |
|
||||
| MonitorRoute | `server/src/routes/v1/monitorRoute.js` | 61 | Endpoint definitions |
|
||||
| Frontend Hooks | `client/src/Hooks/v1/monitorHooks.js` | 500+ | Data fetching |
|
||||
| NetworkService (FE) | `client/src/Utils/NetworkService.js` | 1157 | API client |
|
||||
| Form Hook | `client/src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx` | 97 | Form state |
|
||||
|
||||
---
|
||||
|
||||
## DEPLOYMENT & DOCKER INTEGRATION
|
||||
|
||||
### Docker-Compose Setup
|
||||
**Files:** `/docker/dev/docker-compose.yaml`, `/docker/prod/docker-compose.yaml`
|
||||
|
||||
**Volumes for Docker Monitoring:**
|
||||
```yaml
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock # Required for Dockerode
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
**Backend (.env):**
|
||||
```
|
||||
MONGODB_URI=mongodb://mongo:27017/checkmate
|
||||
JWT_SECRET=your-secret-key
|
||||
NODE_ENV=production|development
|
||||
PORT=5000
|
||||
```
|
||||
|
||||
**Frontend (.env):**
|
||||
```
|
||||
VITE_APP_API_BASE_URL=http://localhost:5000/api/v1
|
||||
```
|
||||
|
||||
@@ -31,14 +31,17 @@ const useInfrastructureMonitorForm = () => {
|
||||
[event.target.name]: event.target.checked,
|
||||
});
|
||||
};
|
||||
const initializeInfrastructureMonitorForCreate = useCallback((globalSettings) => {
|
||||
const initializeInfrastructureMonitorForCreate = useCallback((globalSettings, monitorType = "hardware") => {
|
||||
const gt = globalSettings?.data?.settings?.globalThresholds || {};
|
||||
// Docker monitors default to 30 seconds (0.5 minutes), hardware to 15 seconds (0.25 minutes)
|
||||
const defaultInterval = monitorType === "docker" ? 0.5 : 0.25;
|
||||
|
||||
setInfrastructureMonitor((prev) => ({
|
||||
...prev,
|
||||
url: "",
|
||||
name: "",
|
||||
notifications: [],
|
||||
interval: 0.25,
|
||||
interval: defaultInterval,
|
||||
cpu: gt.cpu !== undefined,
|
||||
usage_cpu: gt.cpu !== undefined ? gt.cpu.toString() : "",
|
||||
memory: gt.memory !== undefined,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
const useInfrastructureSubmit = () => {
|
||||
const [createMonitor, isCreating] = useCreateMonitor();
|
||||
const [updateMonitor, isUpdating] = useUpdateMonitor();
|
||||
const buildForm = (infrastructureMonitor, https) => {
|
||||
const buildForm = (infrastructureMonitor, https, monitorType = "hardware") => {
|
||||
const MS_PER_MINUTE = 60000;
|
||||
|
||||
let form = {
|
||||
@@ -17,31 +17,37 @@ const useInfrastructureSubmit = () => {
|
||||
interval: infrastructureMonitor.interval * MS_PER_MINUTE,
|
||||
statusWindowSize: infrastructureMonitor.statusWindowSize,
|
||||
statusWindowThreshold: infrastructureMonitor.statusWindowThreshold,
|
||||
cpu: infrastructureMonitor.cpu,
|
||||
...(infrastructureMonitor.cpu
|
||||
? { usage_cpu: infrastructureMonitor.usage_cpu }
|
||||
: {}),
|
||||
memory: infrastructureMonitor.memory,
|
||||
...(infrastructureMonitor.memory
|
||||
? { usage_memory: infrastructureMonitor.usage_memory }
|
||||
: {}),
|
||||
disk: infrastructureMonitor.disk,
|
||||
...(infrastructureMonitor.disk
|
||||
? { usage_disk: infrastructureMonitor.usage_disk }
|
||||
: {}),
|
||||
temperature: infrastructureMonitor.temperature,
|
||||
...(infrastructureMonitor.temperature
|
||||
? { usage_temperature: infrastructureMonitor.usage_temperature }
|
||||
: {}),
|
||||
secret: infrastructureMonitor.secret,
|
||||
};
|
||||
|
||||
// Only add hardware-specific thresholds for hardware monitors
|
||||
if (monitorType === "hardware") {
|
||||
form.cpu = infrastructureMonitor.cpu;
|
||||
if (infrastructureMonitor.cpu) {
|
||||
form.usage_cpu = infrastructureMonitor.usage_cpu;
|
||||
}
|
||||
form.memory = infrastructureMonitor.memory;
|
||||
if (infrastructureMonitor.memory) {
|
||||
form.usage_memory = infrastructureMonitor.usage_memory;
|
||||
}
|
||||
form.disk = infrastructureMonitor.disk;
|
||||
if (infrastructureMonitor.disk) {
|
||||
form.usage_disk = infrastructureMonitor.usage_disk;
|
||||
}
|
||||
form.temperature = infrastructureMonitor.temperature;
|
||||
if (infrastructureMonitor.temperature) {
|
||||
form.usage_temperature = infrastructureMonitor.usage_temperature;
|
||||
}
|
||||
}
|
||||
|
||||
return form;
|
||||
};
|
||||
const submitInfrastructureForm = async (
|
||||
infrastructureMonitor,
|
||||
form,
|
||||
isCreate,
|
||||
monitorId
|
||||
monitorId,
|
||||
monitorType = "hardware"
|
||||
) => {
|
||||
const {
|
||||
cpu,
|
||||
@@ -55,20 +61,24 @@ const useInfrastructureSubmit = () => {
|
||||
...rest
|
||||
} = form;
|
||||
|
||||
const thresholds = {
|
||||
// Only build thresholds for hardware monitors
|
||||
const thresholds =
|
||||
monitorType === "hardware"
|
||||
? {
|
||||
...(cpu ? { usage_cpu: usage_cpu / 100 } : {}),
|
||||
...(memory ? { usage_memory: usage_memory / 100 } : {}),
|
||||
...(disk ? { usage_disk: usage_disk / 100 } : {}),
|
||||
...(temperature ? { usage_temperature: usage_temperature / 100 } : {}),
|
||||
};
|
||||
}
|
||||
: {};
|
||||
|
||||
const finalForm = {
|
||||
...(isCreate ? {} : { _id: monitorId }),
|
||||
...rest,
|
||||
description: form.name,
|
||||
type: "hardware",
|
||||
type: monitorType,
|
||||
notifications: infrastructureMonitor.notifications,
|
||||
thresholds,
|
||||
...(Object.keys(thresholds).length > 0 ? { thresholds } : {}),
|
||||
};
|
||||
// Handle create or update
|
||||
isCreate
|
||||
|
||||
@@ -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,6 +29,7 @@ import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit.jsx";
|
||||
|
||||
const CreateInfrastructureMonitor = () => {
|
||||
const { monitorId } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const isCreate = typeof monitorId === "undefined";
|
||||
|
||||
const theme = useTheme();
|
||||
@@ -43,6 +44,11 @@ const CreateInfrastructureMonitor = () => {
|
||||
monitorId,
|
||||
updateTrigger,
|
||||
});
|
||||
|
||||
// Determine monitor type: from query params when creating, from monitor object when editing
|
||||
const monitorType = isCreate
|
||||
? (searchParams.get("type") || "hardware")
|
||||
: (monitor?.type || "hardware");
|
||||
const [deleteMonitor, isDeleting] = useDeleteMonitor();
|
||||
const [globalSettings, globalSettingsLoading] = useFetchGlobalSettings();
|
||||
const [notifications, notificationsAreLoading] = useGetNotificationsByTeamId();
|
||||
@@ -59,7 +65,7 @@ const CreateInfrastructureMonitor = () => {
|
||||
const { buildForm, submitInfrastructureForm, isCreating, isUpdating } =
|
||||
useInfrastructureSubmit();
|
||||
|
||||
const FREQUENCIES = [
|
||||
const ALL_FREQUENCIES = [
|
||||
{ _id: 0.25, name: t("time.fifteenSeconds") },
|
||||
{ _id: 0.5, name: t("time.thirtySeconds") },
|
||||
{ _id: 1, name: t("time.oneMinute") },
|
||||
@@ -67,6 +73,12 @@ const CreateInfrastructureMonitor = () => {
|
||||
{ _id: 5, name: t("time.fiveMinutes") },
|
||||
{ _id: 10, name: t("time.tenMinutes") },
|
||||
];
|
||||
|
||||
// Docker monitors require minimum 30 second interval
|
||||
const FREQUENCIES =
|
||||
monitorType === "docker"
|
||||
? ALL_FREQUENCIES.filter((f) => f._id >= 0.5)
|
||||
: ALL_FREQUENCIES;
|
||||
const CRUMBS = [
|
||||
{ name: "Infrastructure monitors", path: "/infrastructure" },
|
||||
...(isCreate
|
||||
@@ -81,7 +93,8 @@ const CreateInfrastructureMonitor = () => {
|
||||
if (isCreate) {
|
||||
if (globalSettingsLoading) return;
|
||||
setHttps(false);
|
||||
initializeInfrastructureMonitorForCreate(globalSettings);
|
||||
const typeFromQuery = searchParams.get("type") || "hardware";
|
||||
initializeInfrastructureMonitorForCreate(globalSettings, typeFromQuery);
|
||||
} else if (monitor) {
|
||||
setHttps(monitor.url.startsWith("https"));
|
||||
initializeInfrastructureMonitorForUpdate(monitor);
|
||||
@@ -91,6 +104,7 @@ const CreateInfrastructureMonitor = () => {
|
||||
monitor,
|
||||
globalSettings,
|
||||
globalSettingsLoading,
|
||||
searchParams,
|
||||
initializeInfrastructureMonitorForCreate,
|
||||
initializeInfrastructureMonitorForUpdate,
|
||||
]);
|
||||
@@ -98,12 +112,12 @@ const CreateInfrastructureMonitor = () => {
|
||||
// Handlers
|
||||
const onSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
const form = buildForm(infrastructureMonitor, https);
|
||||
const form = buildForm(infrastructureMonitor, https, monitorType);
|
||||
const error = validateForm(form);
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
submitInfrastructureForm(infrastructureMonitor, form, isCreate, monitorId);
|
||||
submitInfrastructureForm(infrastructureMonitor, form, isCreate, monitorId, monitorType);
|
||||
};
|
||||
|
||||
const triggerUpdate = () => {
|
||||
@@ -200,15 +214,25 @@ const CreateInfrastructureMonitor = () => {
|
||||
{t("settingsGeneralSettings")}
|
||||
</Typography>
|
||||
<Typography component="p">
|
||||
{t("infrastructureCreateGeneralSettingsDescription")}
|
||||
{monitorType === "docker"
|
||||
? "Configure your Capture server to monitor Docker containers."
|
||||
: t("infrastructureCreateGeneralSettingsDescription")}
|
||||
</Typography>
|
||||
<Typography component="p">
|
||||
{monitorType === "docker" ? (
|
||||
<>
|
||||
Capture server endpoint: <code>/api/v1/metrics/docker</code>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t("infrastructureServerRequirement")}{" "}
|
||||
<Link
|
||||
level="primary"
|
||||
url="https://github.com/bluewave-labs/checkmate-agent"
|
||||
label={t("common.monitoringAgentName")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
@@ -217,8 +241,16 @@ const CreateInfrastructureMonitor = () => {
|
||||
id="url"
|
||||
name="url"
|
||||
startAdornment={<HttpAdornment https={https} />}
|
||||
placeholder={"localhost:59232/api/v1/metrics"}
|
||||
label={t("infrastructureServerUrlLabel")}
|
||||
placeholder={
|
||||
monitorType === "docker"
|
||||
? "192.168.1.100:59232"
|
||||
: "localhost:59232/api/v1/metrics"
|
||||
}
|
||||
label={
|
||||
monitorType === "docker"
|
||||
? "Capture Server URL"
|
||||
: t("infrastructureServerUrlLabel")
|
||||
}
|
||||
https={https}
|
||||
value={infrastructureMonitor.url}
|
||||
onChange={onChange}
|
||||
@@ -264,7 +296,12 @@ const CreateInfrastructureMonitor = () => {
|
||||
type="text"
|
||||
id="secret"
|
||||
name="secret"
|
||||
label={t("infrastructureAuthorizationSecretLabel")}
|
||||
label={
|
||||
monitorType === "docker"
|
||||
? "Capture API Secret"
|
||||
: t("infrastructureAuthorizationSecretLabel")
|
||||
}
|
||||
placeholder={monitorType === "docker" ? "Bearer token for Capture API" : ""}
|
||||
value={infrastructureMonitor.secret}
|
||||
onChange={onChange}
|
||||
error={errors["secret"] ? true : false}
|
||||
@@ -321,12 +358,14 @@ const CreateInfrastructureMonitor = () => {
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
{monitorType === "hardware" && (
|
||||
<CustomAlertsSection
|
||||
errors={errors}
|
||||
onChange={onChange}
|
||||
infrastructureMonitor={infrastructureMonitor}
|
||||
handleCheckboxChange={handleCheckboxChange}
|
||||
/>
|
||||
)}
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography
|
||||
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
import DataTable from "@/Components/v1/Table/index.jsx";
|
||||
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
|
||||
|
||||
const DockerContainersTable = ({ containers = [], errors = [], captureVersion }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
// Helper to format uptime
|
||||
const formatUptime = (startedAt, finishedAt, running) => {
|
||||
if (!running && finishedAt > 0) {
|
||||
const stoppedAgo = Math.floor(Date.now() / 1000) - finishedAt;
|
||||
return formatDuration(stoppedAgo) + " ago";
|
||||
}
|
||||
if (running && startedAt > 0) {
|
||||
const uptimeSeconds = Math.floor(Date.now() / 1000) - startedAt;
|
||||
return formatDuration(uptimeSeconds);
|
||||
}
|
||||
return "N/A";
|
||||
};
|
||||
|
||||
// Helper to format duration
|
||||
const formatDuration = (seconds) => {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
if (days > 0) return `${days}d ${hours}h`;
|
||||
if (hours > 0) return `${hours}h ${minutes}m`;
|
||||
if (minutes > 0) return `${minutes}m`;
|
||||
return `${seconds}s`;
|
||||
};
|
||||
|
||||
// Helper to get status color
|
||||
const getStatusColor = (status, running, healthy) => {
|
||||
if (status === "running" && healthy) return "success";
|
||||
if (status === "running" && !healthy) return "warning";
|
||||
if (status === "exited") return "error";
|
||||
if (status === "paused") return "warning";
|
||||
if (status === "restarting") return "warning";
|
||||
return "default";
|
||||
};
|
||||
|
||||
const headers = [
|
||||
{
|
||||
id: "name",
|
||||
content: "Container Name",
|
||||
render: (row) => (
|
||||
<Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
fontWeight={600}
|
||||
>
|
||||
{row.container_name || "Unnamed"}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{row.container_id ? row.container_id.substring(0, 12) : "N/A"}
|
||||
</Typography>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: "Status",
|
||||
render: (row) => (
|
||||
<StatusLabel
|
||||
status={getStatusColor(row.status, row.running, row.health?.healthy)}
|
||||
text={row.status || "unknown"}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "health",
|
||||
content: "Health",
|
||||
render: (row) => (
|
||||
<Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={row.health?.healthy ? theme.palette.success.main : theme.palette.error.main}
|
||||
>
|
||||
{row.health?.healthy ? "Healthy" : "Unhealthy"}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{row.health?.source === "container_health_check" ? "HEALTHCHECK" : "State-based"}
|
||||
</Typography>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "image",
|
||||
content: "Image",
|
||||
render: (row) => (
|
||||
<Typography variant="body2">{row.base_image || "N/A"}</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "uptime",
|
||||
content: "Uptime",
|
||||
render: (row) => (
|
||||
<Typography variant="body2">
|
||||
{formatUptime(row.started_at, row.finished_at, row.running)}
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "ports",
|
||||
content: "Ports",
|
||||
render: (row) => (
|
||||
<Box>
|
||||
{row.exposed_ports && row.exposed_ports.length > 0 ? (
|
||||
row.exposed_ports.map((port, idx) => (
|
||||
<Typography
|
||||
key={`${port.port}-${port.protocol}-${idx}`}
|
||||
variant="caption"
|
||||
sx={{ display: "block" }}
|
||||
>
|
||||
{port.port}/{port.protocol}
|
||||
</Typography>
|
||||
))
|
||||
) : (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
None
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// Check if there are critical errors (Docker daemon down)
|
||||
const dockerClientError = errors.find((e) => e.metric && e.metric.includes("docker.client"));
|
||||
|
||||
if (!containers || containers.length === 0) {
|
||||
// Determine the message based on error type
|
||||
let message = "No containers found";
|
||||
let errorDetails = null;
|
||||
|
||||
if (dockerClientError) {
|
||||
message = "Docker daemon unavailable";
|
||||
errorDetails = dockerClientError.err;
|
||||
} else if (errors && errors.length > 0) {
|
||||
message = "Partial data available";
|
||||
errorDetails = "Some containers could not be inspected";
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
padding: theme.spacing(12),
|
||||
textAlign: "center",
|
||||
border: dockerClientError
|
||||
? `2px solid ${theme.palette.error.main}`
|
||||
: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
color={dockerClientError ? theme.palette.error.main : theme.palette.primary.contrastTextTertiary}
|
||||
fontWeight={600}
|
||||
mb={errorDetails ? theme.spacing(2) : 0}
|
||||
>
|
||||
{message}
|
||||
</Typography>
|
||||
{errorDetails && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
>
|
||||
{errorDetails}
|
||||
</Typography>
|
||||
)}
|
||||
{captureVersion && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
sx={{ display: "block", marginTop: theme.spacing(4) }}
|
||||
>
|
||||
Capture version: {captureVersion}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<Box>
|
||||
<Typography
|
||||
variant="h3"
|
||||
fontWeight={600}
|
||||
>
|
||||
Docker Containers ({containers.length})
|
||||
</Typography>
|
||||
{errors && errors.length > 0 && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color={theme.palette.warning.main}
|
||||
sx={{ display: "block", marginTop: theme.spacing(1) }}
|
||||
>
|
||||
⚠ {errors.length} error(s) encountered while fetching container data
|
||||
</Typography>
|
||||
)}
|
||||
{captureVersion && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
sx={{ display: "block", marginTop: theme.spacing(1) }}
|
||||
>
|
||||
Capture version: {captureVersion}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={containers}
|
||||
config={{
|
||||
emptyView: "No containers found",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
DockerContainersTable.propTypes = {
|
||||
containers: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
container_id: PropTypes.string,
|
||||
container_name: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
health: PropTypes.shape({
|
||||
healthy: PropTypes.bool,
|
||||
source: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
}),
|
||||
running: PropTypes.bool,
|
||||
base_image: PropTypes.string,
|
||||
exposed_ports: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
port: PropTypes.string,
|
||||
protocol: PropTypes.string,
|
||||
})
|
||||
),
|
||||
started_at: PropTypes.number,
|
||||
finished_at: PropTypes.number,
|
||||
})
|
||||
),
|
||||
errors: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
metric: PropTypes.arrayOf(PropTypes.string),
|
||||
err: PropTypes.string,
|
||||
})
|
||||
),
|
||||
captureVersion: PropTypes.string,
|
||||
};
|
||||
|
||||
export default DockerContainersTable;
|
||||
@@ -8,6 +8,7 @@ import GaugeBoxes from "./Components/GaugeBoxes/index.jsx";
|
||||
import AreaChartBoxes from "./Components/AreaChartBoxes/index.jsx";
|
||||
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
|
||||
import NetworkStats from "./Components/NetworkStats/index.jsx";
|
||||
import DockerContainersTable from "./Components/DockerContainersTable/index.jsx";
|
||||
import CustomTabList from "@/Components/v1/Tab/index.jsx";
|
||||
import TabContext from "@mui/lab/TabContext";
|
||||
|
||||
@@ -98,12 +99,27 @@ const InfrastructureDetails = () => {
|
||||
label={t("details")}
|
||||
value="details"
|
||||
/>
|
||||
{monitor?.type !== "docker" && (
|
||||
<Tab
|
||||
label={t("network")}
|
||||
value="network"
|
||||
/>
|
||||
)}
|
||||
</CustomTabList>
|
||||
{tab === "details" && (
|
||||
{tab === "details" && monitor?.type === "docker" && (
|
||||
<>
|
||||
<StatusBoxes
|
||||
shouldRender={!isLoading}
|
||||
monitor={monitor}
|
||||
/>
|
||||
<DockerContainersTable
|
||||
containers={monitor?.stats?.aggregateData?.latestCheck?.docker || []}
|
||||
errors={monitor?.stats?.aggregateData?.latestCheck?.errors || []}
|
||||
captureVersion={monitor?.stats?.aggregateData?.latestCheck?.capture?.version}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{tab === "details" && monitor?.type !== "docker" && (
|
||||
<>
|
||||
<StatusBoxes
|
||||
shouldRender={!isLoading}
|
||||
@@ -125,7 +141,7 @@ const InfrastructureDetails = () => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{tab === "network" && (
|
||||
{tab === "network" && monitor?.type !== "docker" && (
|
||||
<NetworkStats
|
||||
net={monitor?.stats?.aggregateData?.latestCheck?.net || []}
|
||||
isLoading={isLoading}
|
||||
|
||||
@@ -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,20 +95,29 @@ 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"
|
||||
path={tab === "docker" ? "/infrastructure/create?type=docker" : "/infrastructure/create?type=hardware"}
|
||||
/>
|
||||
|
||||
<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?type=hardware"
|
||||
>
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Stack direction={"row"}>
|
||||
<MonitorCountHeader
|
||||
isLoading={isLoading}
|
||||
@@ -131,6 +153,55 @@ const InfrastructureMonitors = () => {
|
||||
/>
|
||||
</Stack>
|
||||
</PageStateWrapper>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value="docker" sx={{ padding: 0 }}>
|
||||
<PageStateWrapper
|
||||
networkError={networkError}
|
||||
isLoading={isLoading}
|
||||
items={monitors}
|
||||
type="infrastructureMonitor"
|
||||
fallbackLink="/infrastructure/create?type=docker"
|
||||
>
|
||||
<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