Updated lint errors
Some checks failed
CI/CD Pipeline - Build, Test, and Deploy / 🧪 Test & Lint (push) Failing after 4m51s
CI/CD Pipeline - Build, Test, and Deploy / 🔒 Security Scan (push) Successful in 9m31s
CI/CD Pipeline - Build, Test, and Deploy / 🏗️ Build & Push Image (push) Has been skipped
CI/CD Pipeline - Build, Test, and Deploy / 🛡️ Image Security Scan (push) Has been skipped
CI/CD Pipeline - Build, Test, and Deploy / 🚀 Deploy to Development (push) Has been skipped
CI/CD Pipeline - Build, Test, and Deploy / 🏭 Deploy to Production (push) Has been skipped
CI/CD Pipeline - Build, Test, and Deploy / 🧹 Cleanup (push) Successful in 1s
Some checks failed
CI/CD Pipeline - Build, Test, and Deploy / 🧪 Test & Lint (push) Failing after 4m51s
CI/CD Pipeline - Build, Test, and Deploy / 🔒 Security Scan (push) Successful in 9m31s
CI/CD Pipeline - Build, Test, and Deploy / 🏗️ Build & Push Image (push) Has been skipped
CI/CD Pipeline - Build, Test, and Deploy / 🛡️ Image Security Scan (push) Has been skipped
CI/CD Pipeline - Build, Test, and Deploy / 🚀 Deploy to Development (push) Has been skipped
CI/CD Pipeline - Build, Test, and Deploy / 🏭 Deploy to Production (push) Has been skipped
CI/CD Pipeline - Build, Test, and Deploy / 🧹 Cleanup (push) Successful in 1s
This commit is contained in:
@ -1,34 +1,68 @@
|
|||||||
{
|
{
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"commonjs": true,
|
"commonjs": true,
|
||||||
"es2021": true,
|
"es2021": true,
|
||||||
"node": true,
|
"node": true,
|
||||||
"jest": true
|
"jest": true
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"eslint:recommended"
|
"eslint:recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": "latest"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"indent": [
|
||||||
|
"error",
|
||||||
|
2
|
||||||
],
|
],
|
||||||
"parserOptions": {
|
"linebreak-style": [
|
||||||
"ecmaVersion": "latest"
|
"error",
|
||||||
},
|
"unix"
|
||||||
"rules": {
|
],
|
||||||
"indent": ["error", 2],
|
"quotes": [
|
||||||
"linebreak-style": ["error", "unix"],
|
"error",
|
||||||
"quotes": ["error", "single"],
|
"single"
|
||||||
"semi": ["error", "always"],
|
],
|
||||||
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
|
"semi": [
|
||||||
"no-console": "off",
|
"error",
|
||||||
"no-trailing-spaces": "error",
|
"always"
|
||||||
"eol-last": "error",
|
],
|
||||||
"comma-dangle": ["error", "never"],
|
"no-unused-vars": [
|
||||||
"object-curly-spacing": ["error", "always"],
|
"error",
|
||||||
"array-bracket-spacing": ["error", "never"],
|
{
|
||||||
"space-before-function-paren": ["error", "never"],
|
"argsIgnorePattern": "^_"
|
||||||
"keyword-spacing": "error",
|
}
|
||||||
"space-infix-ops": "error",
|
],
|
||||||
"no-multiple-empty-lines": ["error", { "max": 2 }],
|
"no-console": "off",
|
||||||
"prefer-const": "error",
|
"no-trailing-spaces": "error",
|
||||||
"no-var": "error"
|
"eol-last": "error",
|
||||||
}
|
"comma-dangle": [
|
||||||
}
|
"error",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
|
"object-curly-spacing": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"array-bracket-spacing": [
|
||||||
|
"error",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
|
"space-before-function-paren": [
|
||||||
|
"error",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
|
"keyword-spacing": "error",
|
||||||
|
"space-infix-ops": "error",
|
||||||
|
"no-multiple-empty-lines": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"max": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"prefer-const": "error",
|
||||||
|
"no-var": "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
94
package.json
94
package.json
@ -1,47 +1,53 @@
|
|||||||
{
|
{
|
||||||
"name": "harbor-ci-cd-demo",
|
"name": "harbor-ci-cd-demo",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "System Monitoring Dashboard - Harbor CI/CD Demo",
|
"description": "System Monitoring Dashboard - Harbor CI/CD Demo",
|
||||||
"main": "src/app.js",
|
"main": "src/app.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node src/app.js",
|
"start": "node src/app.js",
|
||||||
"dev": "nodemon src/app.js",
|
"dev": "nodemon src/app.js",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:coverage": "jest --coverage",
|
"test:coverage": "jest --coverage",
|
||||||
"lint": "eslint src/ tests/",
|
"lint": "eslint src/ tests/",
|
||||||
"lint:fix": "eslint src/ tests/ --fix"
|
"lint:fix": "eslint src/ tests/ --fix"
|
||||||
},
|
},
|
||||||
"keywords": ["monitoring", "dashboard", "nodejs", "cicd", "harbor"],
|
"keywords": [
|
||||||
"author": "Your Name",
|
"monitoring",
|
||||||
"license": "MIT",
|
"dashboard",
|
||||||
"dependencies": {
|
"nodejs",
|
||||||
"express": "^4.18.2",
|
"cicd",
|
||||||
"cors": "^2.8.5",
|
"harbor"
|
||||||
"helmet": "^7.1.0",
|
],
|
||||||
"morgan": "^1.10.0",
|
"author": "Your Name",
|
||||||
"compression": "^1.7.4",
|
"license": "MIT",
|
||||||
"dotenv": "^16.3.1"
|
"dependencies": {
|
||||||
},
|
"express": "^4.18.2",
|
||||||
"devDependencies": {
|
"cors": "^2.8.5",
|
||||||
"jest": "^29.7.0",
|
"helmet": "^7.1.0",
|
||||||
"supertest": "^6.3.3",
|
"morgan": "^1.10.0",
|
||||||
"eslint": "^8.53.0",
|
"compression": "^1.7.4",
|
||||||
"nodemon": "^3.0.1"
|
"dotenv": "^16.3.1"
|
||||||
},
|
},
|
||||||
"jest": {
|
"devDependencies": {
|
||||||
"testEnvironment": "node",
|
"jest": "^29.7.0",
|
||||||
"collectCoverageFrom": [
|
"supertest": "^6.3.3",
|
||||||
"src/**/*.js",
|
"eslint": "^8.53.0",
|
||||||
"!src/public/**"
|
"nodemon": "^3.0.1"
|
||||||
],
|
},
|
||||||
"coverageThreshold": {
|
"jest": {
|
||||||
"global": {
|
"testEnvironment": "node",
|
||||||
"branches": 80,
|
"collectCoverageFrom": [
|
||||||
"functions": 80,
|
"src/**/*.js",
|
||||||
"lines": 80,
|
"!src/public/**"
|
||||||
"statements": 80
|
],
|
||||||
}
|
"coverageThreshold": {
|
||||||
|
"global": {
|
||||||
|
"branches": 80,
|
||||||
|
"functions": 80,
|
||||||
|
"lines": 80,
|
||||||
|
"statements": 80
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@ -47,7 +47,7 @@ app.get('/debug/files', (req, res) => {
|
|||||||
url: `http://localhost:${PORT}/${file}`
|
url: `http://localhost:${PORT}/${file}`
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
publicPath,
|
publicPath,
|
||||||
files: fileDetails,
|
files: fileDetails,
|
||||||
@ -101,7 +101,7 @@ app.get('/test-static', (req, res) => {
|
|||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
const indexPath = path.join(publicPath, 'index.html');
|
const indexPath = path.join(publicPath, 'index.html');
|
||||||
console.log('🏠 Serving index.html from:', indexPath);
|
console.log('🏠 Serving index.html from:', indexPath);
|
||||||
|
|
||||||
if (require('fs').existsSync(indexPath)) {
|
if (require('fs').existsSync(indexPath)) {
|
||||||
res.sendFile(indexPath);
|
res.sendFile(indexPath);
|
||||||
} else {
|
} else {
|
||||||
@ -119,7 +119,7 @@ app.use((err, req, res, next) => {
|
|||||||
// 404 handler
|
// 404 handler
|
||||||
app.use('*', (req, res) => {
|
app.use('*', (req, res) => {
|
||||||
console.log('🔍 404 for:', req.originalUrl);
|
console.log('🔍 404 for:', req.originalUrl);
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
error: 'Not Found',
|
error: 'Not Found',
|
||||||
url: req.originalUrl,
|
url: req.originalUrl,
|
||||||
message: 'Static file or route not found'
|
message: 'Static file or route not found'
|
||||||
|
|||||||
@ -3,10 +3,10 @@ const { incrementCounter, recordHistogram } = require('../utils/metrics');
|
|||||||
// Request logging and metrics middleware
|
// Request logging and metrics middleware
|
||||||
function logger(req, res, next) {
|
function logger(req, res, next) {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
// Log request start
|
// Log request start
|
||||||
console.log(`${new Date().toISOString()} - ${req.method} ${req.url} - ${req.ip}`);
|
console.log(`${new Date().toISOString()} - ${req.method} ${req.url} - ${req.ip}`);
|
||||||
|
|
||||||
// Increment request counter
|
// Increment request counter
|
||||||
incrementCounter('http_requests_total', {
|
incrementCounter('http_requests_total', {
|
||||||
method: req.method,
|
method: req.method,
|
||||||
@ -15,24 +15,24 @@ function logger(req, res, next) {
|
|||||||
|
|
||||||
// Override res.end to capture response time and status
|
// Override res.end to capture response time and status
|
||||||
const originalEnd = res.end;
|
const originalEnd = res.end;
|
||||||
res.end = function(chunk, encoding) {
|
res.end = function (chunk, encoding) {
|
||||||
const responseTime = Date.now() - startTime;
|
const responseTime = Date.now() - startTime;
|
||||||
|
|
||||||
// Record response time histogram
|
// Record response time histogram
|
||||||
recordHistogram('http_request_duration_ms', responseTime, {
|
recordHistogram('http_request_duration_ms', responseTime, {
|
||||||
method: req.method,
|
method: req.method,
|
||||||
status_code: res.statusCode
|
status_code: res.statusCode
|
||||||
});
|
});
|
||||||
|
|
||||||
// Increment response counter
|
// Increment response counter
|
||||||
incrementCounter('http_responses_total', {
|
incrementCounter('http_responses_total', {
|
||||||
method: req.method,
|
method: req.method,
|
||||||
status_code: res.statusCode
|
status_code: res.statusCode
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log request completion
|
// Log request completion
|
||||||
console.log(`${new Date().toISOString()} - ${req.method} ${req.url} - ${res.statusCode} - ${responseTime}ms`);
|
console.log(`${new Date().toISOString()} - ${req.method} ${req.url} - ${res.statusCode} - ${responseTime}ms`);
|
||||||
|
|
||||||
// Call original end method
|
// Call original end method
|
||||||
originalEnd.call(this, chunk, encoding);
|
originalEnd.call(this, chunk, encoding);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,113 +1,113 @@
|
|||||||
class MonitoringDashboard {
|
class MonitoringDashboard {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.refreshInterval = null;
|
this.refreshInterval = null;
|
||||||
this.autoRefresh = true;
|
this.autoRefresh = true;
|
||||||
this.refreshRate = 30000; // 30 seconds
|
this.refreshRate = 30000; // 30 seconds
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
await this.loadAllData();
|
await this.loadAllData();
|
||||||
this.startAutoRefresh();
|
this.startAutoRefresh();
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadAllData() {
|
async loadAllData() {
|
||||||
this.updateStatus('loading', 'Loading...');
|
this.updateStatus('loading', 'Loading...');
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.all([
|
|
||||||
this.loadSystemInfo(),
|
|
||||||
this.loadMemoryUsage(),
|
|
||||||
this.loadHealthStatus(),
|
|
||||||
this.loadApiMetrics(),
|
|
||||||
this.loadProcessInfo()
|
|
||||||
]);
|
|
||||||
|
|
||||||
this.updateStatus('healthy', 'System Healthy');
|
|
||||||
this.updateLastUpdated();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading data:', error);
|
|
||||||
this.updateStatus('error', 'Error Loading Data');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadSystemInfo() {
|
try {
|
||||||
try {
|
await Promise.all([
|
||||||
const response = await fetch('/api/system');
|
this.loadSystemInfo(),
|
||||||
const result = await response.json();
|
this.loadMemoryUsage(),
|
||||||
|
this.loadHealthStatus(),
|
||||||
if (result.success) {
|
this.loadApiMetrics(),
|
||||||
this.renderSystemInfo(result.data);
|
this.loadProcessInfo()
|
||||||
} else {
|
]);
|
||||||
throw new Error(result.error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.renderError('system-info', 'Failed to load system information');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadMemoryUsage() {
|
this.updateStatus('healthy', 'System Healthy');
|
||||||
try {
|
this.updateLastUpdated();
|
||||||
const response = await fetch('/api/memory');
|
} catch (error) {
|
||||||
const result = await response.json();
|
console.error('Error loading data:', error);
|
||||||
|
this.updateStatus('error', 'Error Loading Data');
|
||||||
if (result.success) {
|
|
||||||
this.renderMemoryUsage(result.data);
|
|
||||||
} else {
|
|
||||||
throw new Error(result.error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.renderError('memory-usage', 'Failed to load memory data');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async loadHealthStatus() {
|
async loadSystemInfo() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/health/detailed');
|
const response = await fetch('/api/system');
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
this.renderHealthStatus(result);
|
if (result.success) {
|
||||||
this.updateOverallStatus(result.status);
|
this.renderSystemInfo(result.data);
|
||||||
} catch (error) {
|
} else {
|
||||||
this.renderError('health-status', 'Failed to load health data');
|
throw new Error(result.error);
|
||||||
this.updateStatus('error', 'Health Check Failed');
|
}
|
||||||
}
|
} catch (error) {
|
||||||
|
this.renderError('system-info', 'Failed to load system information');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async loadApiMetrics() {
|
async loadMemoryUsage() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/metrics');
|
const response = await fetch('/api/memory');
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
this.renderApiMetrics(result.data);
|
this.renderMemoryUsage(result.data);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.error);
|
throw new Error(result.error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.renderError('api-metrics', 'Failed to load metrics');
|
this.renderError('memory-usage', 'Failed to load memory data');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async loadProcessInfo() {
|
async loadHealthStatus() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/process');
|
const response = await fetch('/health/detailed');
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
this.renderHealthStatus(result);
|
||||||
this.renderProcessInfo(result.data);
|
this.updateOverallStatus(result.status);
|
||||||
} else {
|
} catch (error) {
|
||||||
throw new Error(result.error);
|
this.renderError('health-status', 'Failed to load health data');
|
||||||
}
|
this.updateStatus('error', 'Health Check Failed');
|
||||||
} catch (error) {
|
|
||||||
this.renderError('process-info', 'Failed to load process information');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderSystemInfo(data) {
|
async loadApiMetrics() {
|
||||||
const container = document.getElementById('system-info');
|
try {
|
||||||
container.innerHTML = `
|
const response = await fetch('/api/metrics');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.renderApiMetrics(result.data);
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.renderError('api-metrics', 'Failed to load metrics');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadProcessInfo() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/process');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.renderProcessInfo(result.data);
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.renderError('process-info', 'Failed to load process information');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSystemInfo(data) {
|
||||||
|
const container = document.getElementById('system-info');
|
||||||
|
container.innerHTML = `
|
||||||
<div class="metric-item">
|
<div class="metric-item">
|
||||||
<strong>Hostname:</strong>
|
<strong>Hostname:</strong>
|
||||||
<span class="metric-value">${data.hostname}</span>
|
<span class="metric-value">${data.hostname}</span>
|
||||||
@ -137,18 +137,18 @@ class MonitoringDashboard {
|
|||||||
<span class="metric-value">${data.nodeVersion}</span>
|
<span class="metric-value">${data.nodeVersion}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderMemoryUsage(data) {
|
renderMemoryUsage(data) {
|
||||||
const container = document.getElementById('memory-usage');
|
const container = document.getElementById('memory-usage');
|
||||||
|
|
||||||
const systemPercentage = data.system.percentage;
|
const systemPercentage = data.system.percentage;
|
||||||
const systemClass = systemPercentage > 80 ? 'danger' : systemPercentage > 60 ? 'warning' : '';
|
const systemClass = systemPercentage > 80 ? 'danger' : systemPercentage > 60 ? 'warning' : '';
|
||||||
|
|
||||||
const processHeapPercentage = (data.process.heapUsed / data.process.heapTotal) * 100;
|
const processHeapPercentage = (data.process.heapUsed / data.process.heapTotal) * 100;
|
||||||
const processClass = processHeapPercentage > 80 ? 'danger' : processHeapPercentage > 60 ? 'warning' : '';
|
const processClass = processHeapPercentage > 80 ? 'danger' : processHeapPercentage > 60 ? 'warning' : '';
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div>
|
<div>
|
||||||
<div class="memory-label">
|
<div class="memory-label">
|
||||||
<span>System Memory</span>
|
<span>System Memory</span>
|
||||||
@ -180,15 +180,15 @@ class MonitoringDashboard {
|
|||||||
<span class="metric-value">${data.process.external} MB</span>
|
<span class="metric-value">${data.process.external} MB</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderHealthStatus(data) {
|
renderHealthStatus(data) {
|
||||||
const container = document.getElementById('health-status');
|
const container = document.getElementById('health-status');
|
||||||
|
|
||||||
const statusClass = data.status === 'healthy' ? 'metric-item' :
|
const statusClass = data.status === 'healthy' ? 'metric-item' :
|
||||||
data.status === 'warning' ? 'metric-item warning' : 'metric-item error';
|
data.status === 'warning' ? 'metric-item warning' : 'metric-item error';
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="${statusClass}">
|
<div class="${statusClass}">
|
||||||
<strong>Overall Status:</strong>
|
<strong>Overall Status:</strong>
|
||||||
<span class="health-value">${data.status.toUpperCase()}</span>
|
<span class="health-value">${data.status.toUpperCase()}</span>
|
||||||
@ -214,61 +214,61 @@ class MonitoringDashboard {
|
|||||||
<span class="health-value">${data.loadAverage.join(', ')}</span>
|
<span class="health-value">${data.loadAverage.join(', ')}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderApiMetrics(data) {
|
renderApiMetrics(data) {
|
||||||
const container = document.getElementById('api-metrics');
|
const container = document.getElementById('api-metrics');
|
||||||
|
|
||||||
const requestCounters = data.counters.filter(c => c.name === 'http_requests_total');
|
const requestCounters = data.counters.filter(c => c.name === 'http_requests_total');
|
||||||
const responseCounters = data.counters.filter(c => c.name === 'http_responses_total');
|
const responseCounters = data.counters.filter(c => c.name === 'http_responses_total');
|
||||||
const durationHistograms = data.histograms.filter(h => h.name === 'http_request_duration_ms');
|
const durationHistograms = data.histograms.filter(h => h.name === 'http_request_duration_ms');
|
||||||
|
|
||||||
let html = '<div class="metrics-grid">';
|
let html = '<div class="metrics-grid">';
|
||||||
|
|
||||||
// Total requests
|
// Total requests
|
||||||
const totalRequests = requestCounters.reduce((sum, counter) => sum + counter.value, 0);
|
const totalRequests = requestCounters.reduce((sum, counter) => sum + counter.value, 0);
|
||||||
html += `
|
html += `
|
||||||
<div class="metric-item">
|
<div class="metric-item">
|
||||||
<strong>Total Requests:</strong>
|
<strong>Total Requests:</strong>
|
||||||
<span class="metric-value">${totalRequests}</span>
|
<span class="metric-value">${totalRequests}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Average response time
|
// Average response time
|
||||||
const avgResponseTime = durationHistograms.length > 0 ?
|
const avgResponseTime = durationHistograms.length > 0 ?
|
||||||
durationHistograms.reduce((sum, hist) => sum + hist.average, 0) / durationHistograms.length : 0;
|
durationHistograms.reduce((sum, hist) => sum + hist.average, 0) / durationHistograms.length : 0;
|
||||||
html += `
|
html += `
|
||||||
<div class="metric-item">
|
<div class="metric-item">
|
||||||
<strong>Avg Response Time:</strong>
|
<strong>Avg Response Time:</strong>
|
||||||
<span class="metric-value">${Math.round(avgResponseTime)}ms</span>
|
<span class="metric-value">${Math.round(avgResponseTime)}ms</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Uptime
|
// Uptime
|
||||||
html += `
|
html += `
|
||||||
<div class="metric-item">
|
<div class="metric-item">
|
||||||
<strong>Service Uptime:</strong>
|
<strong>Service Uptime:</strong>
|
||||||
<span class="metric-value">${this.formatUptime(data.runtime.uptime_seconds)}</span>
|
<span class="metric-value">${this.formatUptime(data.runtime.uptime_seconds)}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// API endpoints
|
// API endpoints
|
||||||
const apiCounters = requestCounters.filter(c => c.labels.route && c.labels.route.startsWith('/api'));
|
const apiCounters = requestCounters.filter(c => c.labels.route && c.labels.route.startsWith('/api'));
|
||||||
if (apiCounters.length > 0) {
|
if (apiCounters.length > 0) {
|
||||||
html += '<div class="metric-item"><strong>API Endpoints:</strong><div>';
|
html += '<div class="metric-item"><strong>API Endpoints:</strong><div>';
|
||||||
apiCounters.forEach(counter => {
|
apiCounters.forEach(counter => {
|
||||||
html += `<div>${counter.labels.route}: ${counter.value} calls</div>`;
|
html += `<div>${counter.labels.route}: ${counter.value} calls</div>`;
|
||||||
});
|
});
|
||||||
html += '</div></div>';
|
html += '</div></div>';
|
||||||
}
|
|
||||||
|
|
||||||
html += '</div>';
|
|
||||||
container.innerHTML = html;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderProcessInfo(data) {
|
html += '</div>';
|
||||||
const container = document.getElementById('process-info');
|
container.innerHTML = html;
|
||||||
container.innerHTML = `
|
}
|
||||||
|
|
||||||
|
renderProcessInfo(data) {
|
||||||
|
const container = document.getElementById('process-info');
|
||||||
|
container.innerHTML = `
|
||||||
<div class="metric-item">
|
<div class="metric-item">
|
||||||
<strong>Process ID:</strong>
|
<strong>Process ID:</strong>
|
||||||
<span class="process-value">${data.pid}</span>
|
<span class="process-value">${data.pid}</span>
|
||||||
@ -294,129 +294,129 @@ class MonitoringDashboard {
|
|||||||
<span class="process-value">${data.env}</span>
|
<span class="process-value">${data.env}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderError(containerId, message) {
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
container.innerHTML = `<div class="error">${message}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus(status, text) {
|
||||||
|
const indicator = document.getElementById('status-indicator');
|
||||||
|
const dot = document.getElementById('status-dot');
|
||||||
|
const statusText = document.getElementById('status-text');
|
||||||
|
|
||||||
|
indicator.className = `status-indicator ${status}`;
|
||||||
|
statusText.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOverallStatus(healthStatus) {
|
||||||
|
if (healthStatus === 'healthy') {
|
||||||
|
this.updateStatus('healthy', 'System Healthy');
|
||||||
|
} else if (healthStatus === 'warning') {
|
||||||
|
this.updateStatus('warning', 'System Warning');
|
||||||
|
} else {
|
||||||
|
this.updateStatus('error', 'System Error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLastUpdated() {
|
||||||
|
const element = document.getElementById('last-updated');
|
||||||
|
element.textContent = new Date().toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
formatUptime(seconds) {
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
const secs = Math.floor(seconds % 60);
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
return `${days}d ${hours}h ${minutes}m`;
|
||||||
|
} else if (hours > 0) {
|
||||||
|
return `${hours}h ${minutes}m ${secs}s`;
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return `${minutes}m ${secs}s`;
|
||||||
|
} else {
|
||||||
|
return `${secs}s`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startAutoRefresh() {
|
||||||
|
if (this.refreshInterval) {
|
||||||
|
clearInterval(this.refreshInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderError(containerId, message) {
|
if (this.autoRefresh) {
|
||||||
const container = document.getElementById(containerId);
|
this.refreshInterval = setInterval(() => {
|
||||||
container.innerHTML = `<div class="error">${message}</div>`;
|
this.loadAllData();
|
||||||
|
}, this.refreshRate);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateStatus(status, text) {
|
setupEventListeners() {
|
||||||
const indicator = document.getElementById('status-indicator');
|
// Auto-refresh toggle (could add a button for this)
|
||||||
const dot = document.getElementById('status-dot');
|
document.addEventListener('visibilitychange', () => {
|
||||||
const statusText = document.getElementById('status-text');
|
if (document.hidden) {
|
||||||
|
clearInterval(this.refreshInterval);
|
||||||
indicator.className = `status-indicator ${status}`;
|
} else {
|
||||||
statusText.textContent = text;
|
this.startAutoRefresh();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
updateOverallStatus(healthStatus) {
|
}
|
||||||
if (healthStatus === 'healthy') {
|
|
||||||
this.updateStatus('healthy', 'System Healthy');
|
|
||||||
} else if (healthStatus === 'warning') {
|
|
||||||
this.updateStatus('warning', 'System Warning');
|
|
||||||
} else {
|
|
||||||
this.updateStatus('error', 'System Error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateLastUpdated() {
|
|
||||||
const element = document.getElementById('last-updated');
|
|
||||||
element.textContent = new Date().toLocaleString();
|
|
||||||
}
|
|
||||||
|
|
||||||
formatUptime(seconds) {
|
|
||||||
const days = Math.floor(seconds / 86400);
|
|
||||||
const hours = Math.floor((seconds % 86400) / 3600);
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
|
||||||
const secs = Math.floor(seconds % 60);
|
|
||||||
|
|
||||||
if (days > 0) {
|
|
||||||
return `${days}d ${hours}h ${minutes}m`;
|
|
||||||
} else if (hours > 0) {
|
|
||||||
return `${hours}h ${minutes}m ${secs}s`;
|
|
||||||
} else if (minutes > 0) {
|
|
||||||
return `${minutes}m ${secs}s`;
|
|
||||||
} else {
|
|
||||||
return `${secs}s`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startAutoRefresh() {
|
|
||||||
if (this.refreshInterval) {
|
|
||||||
clearInterval(this.refreshInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.autoRefresh) {
|
|
||||||
this.refreshInterval = setInterval(() => {
|
|
||||||
this.loadAllData();
|
|
||||||
}, this.refreshRate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupEventListeners() {
|
|
||||||
// Auto-refresh toggle (could add a button for this)
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
|
||||||
if (document.hidden) {
|
|
||||||
clearInterval(this.refreshInterval);
|
|
||||||
} else {
|
|
||||||
this.startAutoRefresh();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global functions for buttons
|
// Global functions for buttons
|
||||||
async function refreshData() {
|
async function refreshData() {
|
||||||
const dashboard = window.dashboard;
|
const dashboard = window.dashboard;
|
||||||
if (dashboard) {
|
if (dashboard) {
|
||||||
await dashboard.loadAllData();
|
await dashboard.loadAllData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testAPI() {
|
async function testAPI() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/test');
|
const response = await fetch('/api/test');
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert('✅ API Test Successful!\n\n' +
|
alert('✅ API Test Successful!\n\n' +
|
||||||
`Message: ${result.message}\n` +
|
`Message: ${result.message}\n` +
|
||||||
`Version: ${result.version}\n` +
|
`Version: ${result.version}\n` +
|
||||||
`Environment: ${result.environment}`);
|
`Environment: ${result.environment}`);
|
||||||
} else {
|
} else {
|
||||||
alert('❌ API Test Failed');
|
alert('❌ API Test Failed');
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
alert('❌ API Test Failed: ' + error.message);
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('❌ API Test Failed: ' + error.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadMetrics() {
|
async function downloadMetrics() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/metrics');
|
const response = await fetch('/api/metrics');
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const dataStr = JSON.stringify(result.data, null, 2);
|
const dataStr = JSON.stringify(result.data, null, 2);
|
||||||
const dataBlob = new Blob([dataStr], {type: 'application/json'});
|
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||||
const url = URL.createObjectURL(dataBlob);
|
const url = URL.createObjectURL(dataBlob);
|
||||||
|
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = `metrics-${new Date().toISOString().split('T')[0]}.json`;
|
link.download = `metrics-${new Date().toISOString().split('T')[0]}.json`;
|
||||||
link.click();
|
link.click();
|
||||||
|
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
} else {
|
} else {
|
||||||
alert('Failed to download metrics');
|
alert('Failed to download metrics');
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
alert('Error downloading metrics: ' + error.message);
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error downloading metrics: ' + error.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize dashboard when page loads
|
// Initialize dashboard when page loads
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
window.dashboard = new MonitoringDashboard();
|
window.dashboard = new MonitoringDashboard();
|
||||||
});
|
});
|
||||||
@ -1,81 +1,84 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>System Monitoring Dashboard</title>
|
<title>System Monitoring Dashboard</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1>🖥️ System Monitoring Dashboard</h1>
|
<h1>🖥️ System Monitoring Dashboard</h1>
|
||||||
<p>Real-time system metrics and health monitoring</p>
|
<p>Real-time system metrics and health monitoring</p>
|
||||||
<div class="status-indicator" id="status-indicator">
|
<div class="status-indicator" id="status-indicator">
|
||||||
<span class="status-dot" id="status-dot"></span>
|
<span class="status-dot" id="status-dot"></span>
|
||||||
<span id="status-text">Checking...</span>
|
<span id="status-text">Checking...</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="dashboard-grid">
|
||||||
|
<!-- System Information Card -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>📊 System Information</h2>
|
||||||
|
<div class="metric-grid" id="system-info">
|
||||||
|
<div class="loading">Loading system information...</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</div>
|
||||||
|
|
||||||
<main>
|
<!-- Memory Usage Card -->
|
||||||
<div class="dashboard-grid">
|
<div class="card">
|
||||||
<!-- System Information Card -->
|
<h2>💾 Memory Usage</h2>
|
||||||
<div class="card">
|
<div class="memory-bars" id="memory-usage">
|
||||||
<h2>📊 System Information</h2>
|
<div class="loading">Loading memory data...</div>
|
||||||
<div class="metric-grid" id="system-info">
|
|
||||||
<div class="loading">Loading system information...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Memory Usage Card -->
|
|
||||||
<div class="card">
|
|
||||||
<h2>💾 Memory Usage</h2>
|
|
||||||
<div class="memory-bars" id="memory-usage">
|
|
||||||
<div class="loading">Loading memory data...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Health Status Card -->
|
|
||||||
<div class="card">
|
|
||||||
<h2>❤️ Health Status</h2>
|
|
||||||
<div class="health-grid" id="health-status">
|
|
||||||
<div class="loading">Loading health data...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- API Metrics Card -->
|
|
||||||
<div class="card">
|
|
||||||
<h2>📡 API Metrics</h2>
|
|
||||||
<div class="metrics-grid" id="api-metrics">
|
|
||||||
<div class="loading">Loading metrics...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Process Information Card -->
|
|
||||||
<div class="card">
|
|
||||||
<h2>⚙️ Process Information</h2>
|
|
||||||
<div class="process-grid" id="process-info">
|
|
||||||
<div class="loading">Loading process data...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Quick Actions Card -->
|
|
||||||
<div class="card">
|
|
||||||
<h2>🚀 Quick Actions</h2>
|
|
||||||
<div class="actions-grid">
|
|
||||||
<button onclick="refreshData()" class="btn btn-primary">🔄 Refresh Data</button>
|
|
||||||
<button onclick="testAPI()" class="btn btn-secondary">🧪 Test API</button>
|
|
||||||
<button onclick="downloadMetrics()" class="btn btn-secondary">📥 Download Metrics</button>
|
|
||||||
<a href="/health/detailed" target="_blank" class="btn btn-info">🔍 Detailed Health</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</div>
|
||||||
|
|
||||||
<footer>
|
<!-- Health Status Card -->
|
||||||
<p>Last updated: <span id="last-updated">Never</span></p>
|
<div class="card">
|
||||||
<p>Dashboard built with ❤️ for Harbor CI/CD Demo</p>
|
<h2>❤️ Health Status</h2>
|
||||||
</footer>
|
<div class="health-grid" id="health-status">
|
||||||
|
<div class="loading">Loading health data...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="app.js"></script>
|
<!-- API Metrics Card -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>📡 API Metrics</h2>
|
||||||
|
<div class="metrics-grid" id="api-metrics">
|
||||||
|
<div class="loading">Loading metrics...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Process Information Card -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>⚙️ Process Information</h2>
|
||||||
|
<div class="process-grid" id="process-info">
|
||||||
|
<div class="loading">Loading process data...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions Card -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>🚀 Quick Actions</h2>
|
||||||
|
<div class="actions-grid">
|
||||||
|
<button onclick="refreshData()" class="btn btn-primary">🔄 Refresh Data</button>
|
||||||
|
<button onclick="testAPI()" class="btn btn-secondary">🧪 Test API</button>
|
||||||
|
<button onclick="downloadMetrics()" class="btn btn-secondary">📥 Download Metrics</button>
|
||||||
|
<a href="/health/detailed" target="_blank" class="btn btn-info">🔍 Detailed Health</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>Last updated: <span id="last-updated">Never</span></p>
|
||||||
|
<p>Dashboard built with ❤️ for Harbor CI/CD Demo</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@ -1,276 +1,292 @@
|
|||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
color: #333;
|
color: #333;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
header h1 {
|
header h1 {
|
||||||
color: #4a5568;
|
color: #4a5568;
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
header p {
|
header p {
|
||||||
color: #718096;
|
color: #718096;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-indicator {
|
.status-indicator {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
background: rgba(16, 185, 129, 0.1);
|
background: rgba(16, 185, 129, 0.1);
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border-radius: 25px;
|
border-radius: 25px;
|
||||||
border: 2px solid #10b981;
|
border: 2px solid #10b981;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot {
|
.status-dot {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #10b981;
|
background: #10b981;
|
||||||
animation: pulse 2s infinite;
|
animation: pulse 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-indicator.warning {
|
.status-indicator.warning {
|
||||||
background: rgba(245, 158, 11, 0.1);
|
background: rgba(245, 158, 11, 0.1);
|
||||||
border-color: #f59e0b;
|
border-color: #f59e0b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-indicator.warning .status-dot {
|
.status-indicator.warning .status-dot {
|
||||||
background: #f59e0b;
|
background: #f59e0b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-indicator.error {
|
.status-indicator.error {
|
||||||
background: rgba(239, 68, 68, 0.1);
|
background: rgba(239, 68, 68, 0.1);
|
||||||
border-color: #ef4444;
|
border-color: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-indicator.error .status-dot {
|
.status-indicator.error .status-dot {
|
||||||
background: #ef4444;
|
background: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0.5; }
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 2rem 2rem;
|
padding: 0 2rem 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-grid {
|
.dashboard-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card:hover {
|
.card:hover {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card h2 {
|
.card h2 {
|
||||||
color: #4a5568;
|
color: #4a5568;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
border-bottom: 2px solid #e2e8f0;
|
border-bottom: 2px solid #e2e8f0;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-grid, .health-grid, .process-grid, .metrics-grid {
|
.metric-grid,
|
||||||
display: grid;
|
.health-grid,
|
||||||
gap: 1rem;
|
.process-grid,
|
||||||
|
.metrics-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-item, .health-item, .process-item {
|
.metric-item,
|
||||||
display: flex;
|
.health-item,
|
||||||
justify-content: space-between;
|
.process-item {
|
||||||
align-items: center;
|
display: flex;
|
||||||
padding: 0.75rem;
|
justify-content: space-between;
|
||||||
background: rgba(247, 250, 252, 0.8);
|
align-items: center;
|
||||||
border-radius: 8px;
|
padding: 0.75rem;
|
||||||
border-left: 4px solid #3b82f6;
|
background: rgba(247, 250, 252, 0.8);
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-item strong, .health-item strong, .process-item strong {
|
.metric-item strong,
|
||||||
color: #374151;
|
.health-item strong,
|
||||||
font-weight: 600;
|
.process-item strong {
|
||||||
|
color: #374151;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-value, .health-value, .process-value {
|
.metric-value,
|
||||||
color: #6b7280;
|
.health-value,
|
||||||
font-family: 'Courier New', monospace;
|
.process-value {
|
||||||
font-weight: 500;
|
color: #6b7280;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.memory-bars {
|
.memory-bars {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.memory-bar {
|
.memory-bar {
|
||||||
background: #e5e7eb;
|
background: #e5e7eb;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.memory-bar-fill {
|
.memory-bar-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, #10b981, #059669);
|
background: linear-gradient(90deg, #10b981, #059669);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
transition: width 0.5s ease;
|
transition: width 0.5s ease;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.memory-bar-fill.warning {
|
.memory-bar-fill.warning {
|
||||||
background: linear-gradient(90deg, #f59e0b, #d97706);
|
background: linear-gradient(90deg, #f59e0b, #d97706);
|
||||||
}
|
}
|
||||||
|
|
||||||
.memory-bar-fill.danger {
|
.memory-bar-fill.danger {
|
||||||
background: linear-gradient(90deg, #ef4444, #dc2626);
|
background: linear-gradient(90deg, #ef4444, #dc2626);
|
||||||
}
|
}
|
||||||
|
|
||||||
.memory-label {
|
.memory-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #374151;
|
color: #374151;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions-grid {
|
.actions-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
background: linear-gradient(135deg, #2563eb, #1d4ed8);
|
background: linear-gradient(135deg, #2563eb, #1d4ed8);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: linear-gradient(135deg, #6b7280, #4b5563);
|
background: linear-gradient(135deg, #6b7280, #4b5563);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover {
|
.btn-secondary:hover {
|
||||||
background: linear-gradient(135deg, #4b5563, #374151);
|
background: linear-gradient(135deg, #4b5563, #374151);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-info {
|
.btn-info {
|
||||||
background: linear-gradient(135deg, #10b981, #059669);
|
background: linear-gradient(135deg, #10b981, #059669);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-info:hover {
|
.btn-info:hover {
|
||||||
background: linear-gradient(135deg, #059669, #047857);
|
background: linear-gradient(135deg, #059669, #047857);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
background: rgba(239, 68, 68, 0.1);
|
background: rgba(239, 68, 68, 0.1);
|
||||||
color: #dc2626;
|
color: #dc2626;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border-left: 4px solid #ef4444;
|
border-left: 4px solid #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
color: rgba(255, 255, 255, 0.8);
|
color: rgba(255, 255, 255, 0.8);
|
||||||
background: rgba(0, 0, 0, 0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.dashboard-grid {
|
.dashboard-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
header h1 {
|
header h1 {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
padding: 0 1rem 1rem;
|
padding: 0 1rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions-grid {
|
.actions-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -8,7 +8,7 @@ const router = express.Router();
|
|||||||
router.get('/system', (req, res) => {
|
router.get('/system', (req, res) => {
|
||||||
try {
|
try {
|
||||||
incrementCounter('api_calls_total', { endpoint: '/system' });
|
incrementCounter('api_calls_total', { endpoint: '/system' });
|
||||||
|
|
||||||
const systemInfo = {
|
const systemInfo = {
|
||||||
hostname: os.hostname(),
|
hostname: os.hostname(),
|
||||||
platform: os.platform(),
|
platform: os.platform(),
|
||||||
@ -40,7 +40,7 @@ router.get('/system', (req, res) => {
|
|||||||
router.get('/memory', (req, res) => {
|
router.get('/memory', (req, res) => {
|
||||||
try {
|
try {
|
||||||
incrementCounter('api_calls_total', { endpoint: '/memory' });
|
incrementCounter('api_calls_total', { endpoint: '/memory' });
|
||||||
|
|
||||||
const memoryUsage = process.memoryUsage();
|
const memoryUsage = process.memoryUsage();
|
||||||
const totalMem = os.totalmem();
|
const totalMem = os.totalmem();
|
||||||
const freeMem = os.freemem();
|
const freeMem = os.freemem();
|
||||||
@ -79,7 +79,7 @@ router.get('/memory', (req, res) => {
|
|||||||
router.get('/process', (req, res) => {
|
router.get('/process', (req, res) => {
|
||||||
try {
|
try {
|
||||||
incrementCounter('api_calls_total', { endpoint: '/process' });
|
incrementCounter('api_calls_total', { endpoint: '/process' });
|
||||||
|
|
||||||
const processInfo = {
|
const processInfo = {
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
ppid: process.ppid,
|
ppid: process.ppid,
|
||||||
@ -126,7 +126,7 @@ router.get('/metrics', (req, res) => {
|
|||||||
// Test endpoint for CI/CD validation
|
// Test endpoint for CI/CD validation
|
||||||
router.get('/test', (req, res) => {
|
router.get('/test', (req, res) => {
|
||||||
incrementCounter('api_calls_total', { endpoint: '/test' });
|
incrementCounter('api_calls_total', { endpoint: '/test' });
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'API is working correctly',
|
message: 'API is working correctly',
|
||||||
|
|||||||
@ -18,18 +18,18 @@ router.get('/detailed', (req, res) => {
|
|||||||
const memoryUsage = process.memoryUsage();
|
const memoryUsage = process.memoryUsage();
|
||||||
const totalMem = os.totalmem();
|
const totalMem = os.totalmem();
|
||||||
const freeMem = os.freemem();
|
const freeMem = os.freemem();
|
||||||
|
|
||||||
// Check memory usage (warn if over 80%)
|
// Check memory usage (warn if over 80%)
|
||||||
const memoryPercentage = ((totalMem - freeMem) / totalMem) * 100;
|
const memoryPercentage = ((totalMem - freeMem) / totalMem) * 100;
|
||||||
const memoryStatus = memoryPercentage > 80 ? 'warning' : 'healthy';
|
const memoryStatus = memoryPercentage > 80 ? 'warning' : 'healthy';
|
||||||
|
|
||||||
// Check process memory (warn if heap over 100MB)
|
// Check process memory (warn if heap over 100MB)
|
||||||
const heapUsedMB = memoryUsage.heapUsed / 1024 / 1024;
|
const heapUsedMB = memoryUsage.heapUsed / 1024 / 1024;
|
||||||
const processMemoryStatus = heapUsedMB > 100 ? 'warning' : 'healthy';
|
const processMemoryStatus = heapUsedMB > 100 ? 'warning' : 'healthy';
|
||||||
|
|
||||||
// Overall status
|
// Overall status
|
||||||
const overallStatus = (memoryStatus === 'warning' || processMemoryStatus === 'warning')
|
const overallStatus = (memoryStatus === 'warning' || processMemoryStatus === 'warning')
|
||||||
? 'warning'
|
? 'warning'
|
||||||
: 'healthy';
|
: 'healthy';
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@ -64,9 +64,9 @@ router.get('/detailed', (req, res) => {
|
|||||||
router.get('/ready', (req, res) => {
|
router.get('/ready', (req, res) => {
|
||||||
// Check if the application is ready to serve traffic
|
// Check if the application is ready to serve traffic
|
||||||
// In a real app, you might check database connections, external services, etc.
|
// In a real app, you might check database connections, external services, etc.
|
||||||
|
|
||||||
const isReady = true; // Simplified check
|
const isReady = true; // Simplified check
|
||||||
|
|
||||||
if (isReady) {
|
if (isReady) {
|
||||||
res.json({
|
res.json({
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
|
|||||||
@ -2,94 +2,94 @@
|
|||||||
// In production, you'd use Prometheus, StatsD, or similar
|
// In production, you'd use Prometheus, StatsD, or similar
|
||||||
|
|
||||||
const metrics = {
|
const metrics = {
|
||||||
counters: {},
|
counters: {},
|
||||||
gauges: {},
|
gauges: {},
|
||||||
histograms: {},
|
histograms: {},
|
||||||
startTime: Date.now()
|
startTime: Date.now()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Increment a counter
|
// Increment a counter
|
||||||
function incrementCounter(name, labels = {}) {
|
function incrementCounter(name, labels = {}) {
|
||||||
const key = `${name}_${JSON.stringify(labels)}`;
|
const key = `${name}_${JSON.stringify(labels)}`;
|
||||||
if (!metrics.counters[key]) {
|
if (!metrics.counters[key]) {
|
||||||
metrics.counters[key] = {
|
metrics.counters[key] = {
|
||||||
name,
|
|
||||||
labels,
|
|
||||||
value: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
metrics.counters[key].value++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set a gauge value
|
|
||||||
function setGauge(name, value, labels = {}) {
|
|
||||||
const key = `${name}_${JSON.stringify(labels)}`;
|
|
||||||
metrics.gauges[key] = {
|
|
||||||
name,
|
name,
|
||||||
labels,
|
labels,
|
||||||
value,
|
value: 0
|
||||||
timestamp: Date.now()
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
metrics.counters[key].value++;
|
||||||
// Record histogram value (simplified)
|
}
|
||||||
function recordHistogram(name, value, labels = {}) {
|
|
||||||
const key = `${name}_${JSON.stringify(labels)}`;
|
// Set a gauge value
|
||||||
if (!metrics.histograms[key]) {
|
function setGauge(name, value, labels = {}) {
|
||||||
metrics.histograms[key] = {
|
const key = `${name}_${JSON.stringify(labels)}`;
|
||||||
name,
|
metrics.gauges[key] = {
|
||||||
labels,
|
name,
|
||||||
values: [],
|
labels,
|
||||||
count: 0,
|
value,
|
||||||
sum: 0
|
timestamp: Date.now()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const histogram = metrics.histograms[key];
|
// Record histogram value (simplified)
|
||||||
histogram.values.push(value);
|
function recordHistogram(name, value, labels = {}) {
|
||||||
histogram.count++;
|
const key = `${name}_${JSON.stringify(labels)}`;
|
||||||
histogram.sum += value;
|
if (!metrics.histograms[key]) {
|
||||||
|
metrics.histograms[key] = {
|
||||||
// Keep only last 1000 values to prevent memory issues
|
name,
|
||||||
if (histogram.values.length > 1000) {
|
labels,
|
||||||
histogram.values = histogram.values.slice(-1000);
|
values: [],
|
||||||
}
|
count: 0,
|
||||||
}
|
sum: 0
|
||||||
|
|
||||||
// Get all metrics
|
|
||||||
function getMetrics() {
|
|
||||||
const runtime = {
|
|
||||||
uptime_seconds: Math.round((Date.now() - metrics.startTime) / 1000),
|
|
||||||
memory_usage_bytes: process.memoryUsage(),
|
|
||||||
cpu_usage_percent: process.cpuUsage()
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
counters: Object.values(metrics.counters),
|
|
||||||
gauges: Object.values(metrics.gauges),
|
|
||||||
histograms: Object.values(metrics.histograms).map(h => ({
|
|
||||||
...h,
|
|
||||||
average: h.count > 0 ? h.sum / h.count : 0,
|
|
||||||
min: h.values.length > 0 ? Math.min(...h.values) : 0,
|
|
||||||
max: h.values.length > 0 ? Math.max(...h.values) : 0
|
|
||||||
})),
|
|
||||||
runtime,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset all metrics
|
const histogram = metrics.histograms[key];
|
||||||
function resetMetrics() {
|
histogram.values.push(value);
|
||||||
metrics.counters = {};
|
histogram.count++;
|
||||||
metrics.gauges = {};
|
histogram.sum += value;
|
||||||
metrics.histograms = {};
|
|
||||||
metrics.startTime = Date.now();
|
// Keep only last 1000 values to prevent memory issues
|
||||||
|
if (histogram.values.length > 1000) {
|
||||||
|
histogram.values = histogram.values.slice(-1000);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
module.exports = {
|
|
||||||
incrementCounter,
|
// Get all metrics
|
||||||
setGauge,
|
function getMetrics() {
|
||||||
recordHistogram,
|
const runtime = {
|
||||||
getMetrics,
|
uptime_seconds: Math.round((Date.now() - metrics.startTime) / 1000),
|
||||||
resetMetrics
|
memory_usage_bytes: process.memoryUsage(),
|
||||||
};
|
cpu_usage_percent: process.cpuUsage()
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
counters: Object.values(metrics.counters),
|
||||||
|
gauges: Object.values(metrics.gauges),
|
||||||
|
histograms: Object.values(metrics.histograms).map(h => ({
|
||||||
|
...h,
|
||||||
|
average: h.count > 0 ? h.sum / h.count : 0,
|
||||||
|
min: h.values.length > 0 ? Math.min(...h.values) : 0,
|
||||||
|
max: h.values.length > 0 ? Math.max(...h.values) : 0
|
||||||
|
})),
|
||||||
|
runtime,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset all metrics
|
||||||
|
function resetMetrics() {
|
||||||
|
metrics.counters = {};
|
||||||
|
metrics.gauges = {};
|
||||||
|
metrics.histograms = {};
|
||||||
|
metrics.startTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
incrementCounter,
|
||||||
|
setGauge,
|
||||||
|
recordHistogram,
|
||||||
|
getMetrics,
|
||||||
|
resetMetrics
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user