Add System Monitoring Dashboard with Harbor CI/CD integration
Some checks failed
CI/CD Pipeline - Build, Test, and Deploy / 🧪 Test & Lint (push) Failing after 5m24s
CI/CD Pipeline - Build, Test, and Deploy / 🔒 Security Scan (push) Successful in 10m1s
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:
2025-07-02 15:03:15 -06:00
parent 01c3d9992e
commit daf3dbe0ef
17 changed files with 8238 additions and 1 deletions

34
.eslintrc.json Normal file
View File

@ -0,0 +1,34 @@
{
"env": {
"browser": true,
"commonjs": true,
"es2021": true,
"node": true,
"jest": true
},
"extends": [
"eslint:recommended"
],
"parserOptions": {
"ecmaVersion": "latest"
},
"rules": {
"indent": ["error", 2],
"linebreak-style": ["error", "unix"],
"quotes": ["error", "single"],
"semi": ["error", "always"],
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"no-console": "off",
"no-trailing-spaces": "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"
}
}

236
.gitea/workflows/ci-cd.yml Normal file
View File

@ -0,0 +1,236 @@
name: CI/CD Pipeline - Build, Test, and Deploy
on:
push:
branches: [main, master, develop]
pull_request:
branches: [main, master]
workflow_dispatch:
env:
NODE_VERSION: '18'
REGISTRY: ${{ secrets.HARBOR_REGISTRY }}
IMAGE_NAME: infrastructure/monitoring-dashboard
jobs:
# Job 1: Lint and Test
test:
name: 🧪 Test & Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linting
run: npm run lint
- name: Run tests
run: npm run test:coverage
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: test-results
path: |
coverage/
test-results.xml
# Job 2: Security Scan
security:
name: 🔒 Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run security audit
run: npm audit --audit-level=high
- name: Check for vulnerabilities
run: |
if npm audit --audit-level=moderate --json | jq '.vulnerabilities | length' | grep -v '^0$'; then
echo "Vulnerabilities found!"
npm audit --audit-level=moderate
exit 1
fi
# Job 3: Build and Push Docker Image
build:
name: 🏗️ Build & Push Image
runs-on: ubuntu-latest
needs: [test, security]
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
image-digest: ${{ steps.build.outputs.digest }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Harbor Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.HARBOR_USERNAME }}
password: ${{ secrets.HARBOR_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value={{date 'YYYYMMDD-HHmmss'}}
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
format: spdx-json
output-file: sbom.spdx.json
- name: Upload SBOM
uses: actions/upload-artifact@v3
with:
name: sbom
path: sbom.spdx.json
# Job 4: Image Security Scan
scan:
name: 🛡️ Image Security Scan
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
steps:
- name: Login to Harbor Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.HARBOR_USERNAME }}
password: ${{ secrets.HARBOR_TOKEN }}
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy scan results
uses: actions/upload-artifact@v3
with:
name: trivy-scan-results
path: trivy-results.sarif
- name: Check for HIGH/CRITICAL vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
format: 'json'
output: 'trivy-results.json'
exit-code: '1'
severity: 'HIGH,CRITICAL'
# Job 5: Deploy to Development
deploy-dev:
name: 🚀 Deploy to Development
runs-on: ubuntu-latest
needs: [build, scan]
if: github.ref == 'refs/heads/develop'
environment: development
steps:
- name: Deploy to development environment
run: |
echo "🚀 Deploying to development environment"
echo "Image: ${{ needs.build.outputs.image-tag }}"
echo "Digest: ${{ needs.build.outputs.image-digest }}"
# Add actual deployment commands here
# For example: kubectl, docker-compose, or API calls
# Job 6: Deploy to Production
deploy-prod:
name: 🏭 Deploy to Production
runs-on: ubuntu-latest
needs: [build, scan]
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
environment: production
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy to production
run: |
echo "🏭 Deploying to production environment"
echo "Image: ${{ needs.build.outputs.image-tag }}"
echo "Digest: ${{ needs.build.outputs.image-digest }}"
# Example deployment script
# In a real scenario, you might:
# 1. SSH to your server
# 2. Pull the new image
# 3. Update docker-compose.yml
# 4. Restart the service
# 5. Run health checks
- name: Health check after deployment
run: |
echo "🔍 Running post-deployment health checks"
# Add health check commands here
# curl -f http://your-app-url/health || exit 1
- name: Notify deployment success
run: |
echo "✅ Deployment completed successfully!"
echo "🌐 Application URL: https://your-domain.com"
echo "📊 Monitoring: https://your-domain.com/health/detailed"
# Job 7: Cleanup
cleanup:
name: 🧹 Cleanup
runs-on: ubuntu-latest
needs: [deploy-dev, deploy-prod]
if: always()
steps:
- name: Clean up old images
run: |
echo "🧹 Cleaning up old container images"
# Add cleanup logic here
# For example, remove images older than 30 days

32
Dockerfile Normal file
View File

@ -0,0 +1,32 @@
# Use official Node.js runtime as base image
FROM node:18-alpine
# Set working directory in container
WORKDIR /app
# Copy package files first (for better Docker layer caching)
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production && npm cache clean --force
# Create non-root user for security
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Copy application code
COPY src/ ./src/
# Change ownership to nodejs user
RUN chown -R nodejs:nodejs /app
USER nodejs
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"
# Start the application
CMD ["npm", "start"]

286
README.md
View File

@ -1,3 +1,289 @@
# harbor-ci-cd-demo
Learning Harbor + Gitea Actions CI/CD integration
# 🖥️ System Monitoring Dashboard
A comprehensive Node.js application for real-time system monitoring with a beautiful web dashboard. Built to demonstrate Harbor CI/CD integration with Gitea Actions.
## ✨ Features
- **🚀 Real-time System Monitoring**: CPU, memory, disk usage, and load averages
- **📊 Interactive Dashboard**: Beautiful, responsive web interface with live data
- **🔍 Health Checks**: Kubernetes-style readiness and liveness probes
- **📡 REST API**: Complete API for system metrics and monitoring data
- **🧪 Comprehensive Testing**: Unit tests with coverage reporting
- **🔒 Security**: Helmet.js security headers, input validation
- **📈 Metrics Collection**: Request tracking, response times, error rates
- **🐳 Container Ready**: Optimized Docker configuration
- **⚡ Production Ready**: Proper error handling, logging, and monitoring
## 🚀 Quick Start
### Prerequisites
- Node.js 18+
- Docker (optional)
- Git
### Local Development
```bash
# Clone the repository
git clone https://git.maverickapplications.com/[username]/harbor-ci-cd-demo.git
cd harbor-ci-cd-demo
# Install dependencies
npm install
# Start development server
npm run dev
# Open your browser
open http://localhost:3000
```
### Using Docker
```bash
# Build and run with Docker
docker build -t monitoring-dashboard .
docker run -p 3000:3000 monitoring-dashboard
# Or use Docker Compose
docker-compose up
```
## 📚 API Documentation
### System Information
- **GET /api/system** - System hardware and OS information
- **GET /api/memory** - Memory usage statistics
- **GET /api/process** - Node.js process information
- **GET /api/metrics** - Application metrics and counters
### Health Checks
- **GET /health** - Basic health status
- **GET /health/detailed** - Comprehensive health information
- **GET /health/ready** - Readiness probe (Kubernetes compatible)
- **GET /health/live** - Liveness probe (Kubernetes compatible)
### Testing
- **GET /api/test** - API connectivity test endpoint
## 🧪 Testing
```bash
# Run all tests
npm test
# Run tests with coverage
npm run test:coverage
# Run tests in watch mode
npm run test:watch
# Run linting
npm run lint
# Fix linting issues
npm run lint:fix
```
## 📊 Dashboard Features
### System Overview
- Hostname, platform, and architecture
- CPU cores and load averages
- Total system memory
- System uptime
- Node.js version information
### Memory Monitoring
- System memory usage with visual bars
- Process heap memory tracking
- RSS and external memory statistics
- Warning indicators for high usage
### Health Status
- Overall system health indicator
- Memory status monitoring
- Process uptime tracking
- Environment information
### API Metrics
- Request counters by endpoint
- Average response times
- Service uptime tracking
- Real-time performance data
### Process Information
- Process ID and parent process
- Working directory
- Node.js version details
- Environment configuration
## 🔧 Configuration
### Environment Variables
```bash
PORT=3000 # Server port (default: 3000)
NODE_ENV=production # Environment mode
```
### Docker Environment
```dockerfile
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
```
## 🐳 Container Deployment
### Building the Image
```bash
# Build locally
docker build -t monitoring-dashboard:latest .
# Build for Harbor registry
docker build -t registry.maverickapplications.com/infrastructure/monitoring-dashboard:latest .
```
### Running in Production
```bash
# Run with resource limits
docker run -d \
--name monitoring-dashboard \
--memory=512m \
--cpus=1 \
-p 3000:3000 \
-e NODE_ENV=production \
--restart=unless-stopped \
monitoring-dashboard:latest
```
## 🔄 CI/CD Pipeline
This application includes a complete CI/CD pipeline with:
- **🧪 Automated Testing**: Unit tests and coverage reporting
- **🔒 Security Scanning**: npm audit and vulnerability checks
- **🏗️ Multi-platform Builds**: AMD64 and ARM64 support
- **📦 Harbor Integration**: Automatic image building and pushing
- **🛡️ Vulnerability Scanning**: Trivy security scanning
- **🚀 Automated Deployment**: Environment-based deployment
- **📋 SBOM Generation**: Software Bill of Materials
### Pipeline Stages
1. **Test & Lint** - Code quality and functionality validation
2. **Security Scan** - Dependency vulnerability checking
3. **Build & Push** - Multi-platform Docker image creation
4. **Image Scan** - Container vulnerability assessment
5. **Deploy** - Environment-specific deployment
6. **Cleanup** - Resource management
## 📈 Monitoring & Observability
### Health Check Endpoints
```bash
# Basic health check
curl http://localhost:3000/health
# Detailed health information
curl http://localhost:3000/health/detailed
# Kubernetes readiness probe
curl http://localhost:3000/health/ready
# Kubernetes liveness probe
curl http://localhost:3000/health/live
```
### Metrics Collection
The application automatically tracks:
- HTTP request counters
- Response time histograms
- Error rate monitoring
- Memory usage patterns
- System resource utilization
### Dashboard Access
- **Main Dashboard**: http://localhost:3000
- **API Documentation**: http://localhost:3000/api
- **Health Status**: http://localhost:3000/health/detailed
## 🛠️ Development
### Project Structure
```
harbor-ci-cd-demo/
├── .gitea/workflows/ # CI/CD pipeline configuration
├── src/ # Application source code
│ ├── routes/ # API route handlers
│ ├── middleware/ # Custom middleware
│ ├── utils/ # Utility functions
│ └── public/ # Frontend assets
├── tests/ # Test suites
├── Dockerfile # Container configuration
├── docker-compose.yml # Local development setup
└── package.json # Node.js configuration
```
### Adding New Features
1. **API Endpoints**: Add routes in `src/routes/`
2. **Middleware**: Create middleware in `src/middleware/`
3. **Tests**: Add tests in `tests/` directory
4. **Frontend**: Update dashboard in `src/public/`
### Code Style
- ESLint configuration with recommended rules
- Prettier formatting (optional)
- Jest testing framework
- Security-first approach
## 🔒 Security Features
- **Helmet.js**: Security headers and protections
- **CORS**: Cross-origin resource sharing control
- **Input Validation**: Request data validation
- **Error Handling**: Secure error responses
- **Health Checks**: System monitoring capabilities
- **Container Security**: Non-root user execution
## 📖 Learn More
This application demonstrates:
- **Modern Node.js Development**: ES6+, async/await, modules
- **RESTful API Design**: Resource-based endpoints
- **Real-time Monitoring**: Live system metrics
- **Container Best Practices**: Multi-stage builds, security
- **CI/CD Integration**: Automated testing and deployment
- **Production Readiness**: Logging, monitoring, health checks
## 🤝 Contributing
1. Fork the repository
2. Create a feature branch
3. Add tests for new functionality
4. Ensure all tests pass
5. Submit a pull request
## 📄 License
MIT License - see LICENSE file for details
---
**Built with ❤️ for Harbor CI/CD Demo**
This application showcases enterprise-grade Node.js development with comprehensive monitoring, testing, and deployment automation.

43
docker-compose.yml Normal file
View File

@ -0,0 +1,43 @@
version: '3.8'
services:
monitoring-dashboard:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- PORT=3000
volumes:
- ./src:/app/src:ro
- ./package.json:/app/package.json:ro
- ./package-lock.json:/app/package-lock.json:ro
restart: unless-stopped
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
- monitoring
# Optional: Add a reverse proxy for local development
nginx:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- monitoring-dashboard
networks:
- monitoring
profiles:
- with-proxy
networks:
monitoring:
driver: bridge

6081
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

47
package.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "harbor-ci-cd-demo",
"version": "1.0.0",
"description": "System Monitoring Dashboard - Harbor CI/CD Demo",
"main": "src/app.js",
"scripts": {
"start": "node src/app.js",
"dev": "nodemon src/app.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src/ tests/",
"lint:fix": "eslint src/ tests/ --fix"
},
"keywords": ["monitoring", "dashboard", "nodejs", "cicd", "harbor"],
"author": "Your Name",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"helmet": "^7.1.0",
"morgan": "^1.10.0",
"compression": "^1.7.4",
"dotenv": "^16.3.1"
},
"devDependencies": {
"jest": "^29.7.0",
"supertest": "^6.3.3",
"eslint": "^8.53.0",
"nodemon": "^3.0.1"
},
"jest": {
"testEnvironment": "node",
"collectCoverageFrom": [
"src/**/*.js",
"!src/public/**"
],
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}

139
src/app.js Normal file
View File

@ -0,0 +1,139 @@
const express = require('express');
const path = require('path');
const cors = require('cors');
const morgan = require('morgan');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3000;
// Basic middleware
app.use(cors());
app.use(express.json());
app.use(morgan('dev'));
// Debug: Log the static files directory
const publicPath = path.join(__dirname, 'public');
console.log('📁 Static files directory:', publicPath);
console.log('📁 Directory exists:', require('fs').existsSync(publicPath));
// List files in public directory
try {
const files = require('fs').readdirSync(publicPath);
console.log('📄 Files in public directory:', files);
} catch (error) {
console.error('❌ Error reading public directory:', error.message);
}
// Serve static files with debugging
app.use('/', express.static(publicPath, {
setHeaders: (res, filePath) => {
console.log('📤 Serving static file:', filePath);
res.setHeader('Cache-Control', 'no-cache');
}
}));
// Debug route to test file serving
app.get('/debug/files', (req, res) => {
try {
const files = require('fs').readdirSync(publicPath);
const fileDetails = files.map(file => {
const filePath = path.join(publicPath, file);
const stats = require('fs').statSync(filePath);
return {
name: file,
size: stats.size,
path: filePath,
url: `http://localhost:${PORT}/${file}`
};
});
res.json({
publicPath,
files: fileDetails,
timestamp: new Date().toISOString()
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Import routes after static files
try {
const apiRoutes = require('./routes/api');
const healthRoutes = require('./routes/health');
app.use('/api', apiRoutes);
app.use('/health', healthRoutes);
} catch (error) {
console.error('⚠️ Routes not loaded:', error.message);
}
// Test endpoints
app.get('/test-static', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Static File Test</title>
<style>body { font-family: Arial; padding: 20px; }</style>
</head>
<body>
<h1>Static File Test</h1>
<p>If you can see this page, the server is working.</p>
<h2>Test Links:</h2>
<ul>
<li><a href="/style.css" target="_blank">style.css</a></li>
<li><a href="/app.js" target="_blank">app.js</a></li>
<li><a href="/index.html" target="_blank">index.html</a></li>
<li><a href="/debug/files" target="_blank">File Debug Info</a></li>
</ul>
<h2>API Tests:</h2>
<ul>
<li><a href="/api/system" target="_blank">API System Info</a></li>
<li><a href="/health" target="_blank">Health Check</a></li>
</ul>
</body>
</html>
`);
});
// Root route
app.get('/', (req, res) => {
const indexPath = path.join(publicPath, 'index.html');
console.log('🏠 Serving index.html from:', indexPath);
if (require('fs').existsSync(indexPath)) {
res.sendFile(indexPath);
} else {
console.error('❌ index.html not found at:', indexPath);
res.redirect('/test-static');
}
});
// Error handler
app.use((err, req, res, next) => {
console.error('💥 Error:', err);
res.status(500).json({ error: err.message });
});
// 404 handler
app.use('*', (req, res) => {
console.log('🔍 404 for:', req.originalUrl);
res.status(404).json({
error: 'Not Found',
url: req.originalUrl,
message: 'Static file or route not found'
});
});
// Start server
if (require.main === module) {
app.listen(PORT, () => {
console.log(`🚀 Server running on port ${PORT}`);
console.log(`🧪 Test page: http://localhost:${PORT}/test-static`);
console.log(`📊 Dashboard: http://localhost:${PORT}`);
console.log(`🔧 Debug info: http://localhost:${PORT}/debug/files`);
});
}
module.exports = app;

43
src/middleware/logger.js Normal file
View File

@ -0,0 +1,43 @@
const { incrementCounter, recordHistogram } = require('../utils/metrics');
// Request logging and metrics middleware
function logger(req, res, next) {
const startTime = Date.now();
// Log request start
console.log(`${new Date().toISOString()} - ${req.method} ${req.url} - ${req.ip}`);
// Increment request counter
incrementCounter('http_requests_total', {
method: req.method,
route: req.route?.path || req.url
});
// Override res.end to capture response time and status
const originalEnd = res.end;
res.end = function(chunk, encoding) {
const responseTime = Date.now() - startTime;
// Record response time histogram
recordHistogram('http_request_duration_ms', responseTime, {
method: req.method,
status_code: res.statusCode
});
// Increment response counter
incrementCounter('http_responses_total', {
method: req.method,
status_code: res.statusCode
});
// Log request completion
console.log(`${new Date().toISOString()} - ${req.method} ${req.url} - ${res.statusCode} - ${responseTime}ms`);
// Call original end method
originalEnd.call(this, chunk, encoding);
};
next();
}
module.exports = logger;

422
src/public/app.js Normal file
View File

@ -0,0 +1,422 @@
class MonitoringDashboard {
constructor() {
this.refreshInterval = null;
this.autoRefresh = true;
this.refreshRate = 30000; // 30 seconds
this.init();
}
async init() {
await this.loadAllData();
this.startAutoRefresh();
this.setupEventListeners();
}
async loadAllData() {
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 {
const response = await fetch('/api/system');
const result = await response.json();
if (result.success) {
this.renderSystemInfo(result.data);
} else {
throw new Error(result.error);
}
} catch (error) {
this.renderError('system-info', 'Failed to load system information');
}
}
async loadMemoryUsage() {
try {
const response = await fetch('/api/memory');
const result = await response.json();
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() {
try {
const response = await fetch('/health/detailed');
const result = await response.json();
this.renderHealthStatus(result);
this.updateOverallStatus(result.status);
} catch (error) {
this.renderError('health-status', 'Failed to load health data');
this.updateStatus('error', 'Health Check Failed');
}
}
async loadApiMetrics() {
try {
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">
<strong>Hostname:</strong>
<span class="metric-value">${data.hostname}</span>
</div>
<div class="metric-item">
<strong>Platform:</strong>
<span class="metric-value">${data.platform} ${data.architecture}</span>
</div>
<div class="metric-item">
<strong>CPU Cores:</strong>
<span class="metric-value">${data.cpus}</span>
</div>
<div class="metric-item">
<strong>Total Memory:</strong>
<span class="metric-value">${data.totalMemory} GB</span>
</div>
<div class="metric-item">
<strong>System Uptime:</strong>
<span class="metric-value">${this.formatUptime(data.uptime)}</span>
</div>
<div class="metric-item">
<strong>Load Average:</strong>
<span class="metric-value">${data.loadAverage.map(l => l.toFixed(2)).join(', ')}</span>
</div>
<div class="metric-item">
<strong>Node.js Version:</strong>
<span class="metric-value">${data.nodeVersion}</span>
</div>
`;
}
renderMemoryUsage(data) {
const container = document.getElementById('memory-usage');
const systemPercentage = data.system.percentage;
const systemClass = systemPercentage > 80 ? 'danger' : systemPercentage > 60 ? 'warning' : '';
const processHeapPercentage = (data.process.heapUsed / data.process.heapTotal) * 100;
const processClass = processHeapPercentage > 80 ? 'danger' : processHeapPercentage > 60 ? 'warning' : '';
container.innerHTML = `
<div>
<div class="memory-label">
<span>System Memory</span>
<span>${data.system.used} GB / ${data.system.total} GB (${systemPercentage}%)</span>
</div>
<div class="memory-bar">
<div class="memory-bar-fill ${systemClass}" style="width: ${systemPercentage}%">
${systemPercentage}%
</div>
</div>
</div>
<div>
<div class="memory-label">
<span>Process Heap</span>
<span>${data.process.heapUsed} MB / ${data.process.heapTotal} MB</span>
</div>
<div class="memory-bar">
<div class="memory-bar-fill ${processClass}" style="width: ${processHeapPercentage}%">
${Math.round(processHeapPercentage)}%
</div>
</div>
</div>
<div class="metric-item">
<strong>RSS Memory:</strong>
<span class="metric-value">${data.process.rss} MB</span>
</div>
<div class="metric-item">
<strong>External Memory:</strong>
<span class="metric-value">${data.process.external} MB</span>
</div>
`;
}
renderHealthStatus(data) {
const container = document.getElementById('health-status');
const statusClass = data.status === 'healthy' ? 'metric-item' :
data.status === 'warning' ? 'metric-item warning' : 'metric-item error';
container.innerHTML = `
<div class="${statusClass}">
<strong>Overall Status:</strong>
<span class="health-value">${data.status.toUpperCase()}</span>
</div>
<div class="metric-item">
<strong>Service Version:</strong>
<span class="health-value">${data.version}</span>
</div>
<div class="metric-item">
<strong>Process Uptime:</strong>
<span class="health-value">${this.formatUptime(data.uptime.process)}</span>
</div>
<div class="metric-item">
<strong>Environment:</strong>
<span class="health-value">${data.environment}</span>
</div>
<div class="metric-item">
<strong>Memory Status:</strong>
<span class="health-value">${data.memory.status.toUpperCase()}</span>
</div>
<div class="metric-item">
<strong>Load Average:</strong>
<span class="health-value">${data.loadAverage.join(', ')}</span>
</div>
`;
}
renderApiMetrics(data) {
const container = document.getElementById('api-metrics');
const requestCounters = data.counters.filter(c => c.name === 'http_requests_total');
const responseCounters = data.counters.filter(c => c.name === 'http_responses_total');
const durationHistograms = data.histograms.filter(h => h.name === 'http_request_duration_ms');
let html = '<div class="metrics-grid">';
// Total requests
const totalRequests = requestCounters.reduce((sum, counter) => sum + counter.value, 0);
html += `
<div class="metric-item">
<strong>Total Requests:</strong>
<span class="metric-value">${totalRequests}</span>
</div>
`;
// Average response time
const avgResponseTime = durationHistograms.length > 0 ?
durationHistograms.reduce((sum, hist) => sum + hist.average, 0) / durationHistograms.length : 0;
html += `
<div class="metric-item">
<strong>Avg Response Time:</strong>
<span class="metric-value">${Math.round(avgResponseTime)}ms</span>
</div>
`;
// Uptime
html += `
<div class="metric-item">
<strong>Service Uptime:</strong>
<span class="metric-value">${this.formatUptime(data.runtime.uptime_seconds)}</span>
</div>
`;
// API endpoints
const apiCounters = requestCounters.filter(c => c.labels.route && c.labels.route.startsWith('/api'));
if (apiCounters.length > 0) {
html += '<div class="metric-item"><strong>API Endpoints:</strong><div>';
apiCounters.forEach(counter => {
html += `<div>${counter.labels.route}: ${counter.value} calls</div>`;
});
html += '</div></div>';
}
html += '</div>';
container.innerHTML = html;
}
renderProcessInfo(data) {
const container = document.getElementById('process-info');
container.innerHTML = `
<div class="metric-item">
<strong>Process ID:</strong>
<span class="process-value">${data.pid}</span>
</div>
<div class="metric-item">
<strong>Parent PID:</strong>
<span class="process-value">${data.ppid}</span>
</div>
<div class="metric-item">
<strong>Process Uptime:</strong>
<span class="process-value">${this.formatUptime(data.uptime)}</span>
</div>
<div class="metric-item">
<strong>Working Directory:</strong>
<span class="process-value">${data.cwd}</span>
</div>
<div class="metric-item">
<strong>Node Version:</strong>
<span class="process-value">${data.version}</span>
</div>
<div class="metric-item">
<strong>Environment:</strong>
<span class="process-value">${data.env}</span>
</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);
}
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
async function refreshData() {
const dashboard = window.dashboard;
if (dashboard) {
await dashboard.loadAllData();
}
}
async function testAPI() {
try {
const response = await fetch('/api/test');
const result = await response.json();
if (result.success) {
alert('✅ API Test Successful!\n\n' +
`Message: ${result.message}\n` +
`Version: ${result.version}\n` +
`Environment: ${result.environment}`);
} else {
alert('❌ API Test Failed');
}
} catch (error) {
alert('❌ API Test Failed: ' + error.message);
}
}
async function downloadMetrics() {
try {
const response = await fetch('/api/metrics');
const result = await response.json();
if (result.success) {
const dataStr = JSON.stringify(result.data, null, 2);
const dataBlob = new Blob([dataStr], {type: 'application/json'});
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `metrics-${new Date().toISOString().split('T')[0]}.json`;
link.click();
URL.revokeObjectURL(url);
} else {
alert('Failed to download metrics');
}
} catch (error) {
alert('Error downloading metrics: ' + error.message);
}
}
// Initialize dashboard when page loads
document.addEventListener('DOMContentLoaded', () => {
window.dashboard = new MonitoringDashboard();
});

81
src/public/index.html Normal file
View File

@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>System Monitoring Dashboard</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>🖥️ System Monitoring Dashboard</h1>
<p>Real-time system metrics and health monitoring</p>
<div class="status-indicator" id="status-indicator">
<span class="status-dot" id="status-dot"></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>
<!-- 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>
</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>
</html>

276
src/public/style.css Normal file
View File

@ -0,0 +1,276 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #333;
min-height: 100vh;
line-height: 1.6;
}
header {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
padding: 2rem;
text-align: center;
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
}
header h1 {
color: #4a5568;
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
header p {
color: #718096;
font-size: 1.1rem;
margin-bottom: 1rem;
}
.status-indicator {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: rgba(16, 185, 129, 0.1);
padding: 0.5rem 1rem;
border-radius: 25px;
border: 2px solid #10b981;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #10b981;
animation: pulse 2s infinite;
}
.status-indicator.warning {
background: rgba(245, 158, 11, 0.1);
border-color: #f59e0b;
}
.status-indicator.warning .status-dot {
background: #f59e0b;
}
.status-indicator.error {
background: rgba(239, 68, 68, 0.1);
border-color: #ef4444;
}
.status-indicator.error .status-dot {
background: #ef4444;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
main {
max-width: 1400px;
margin: 0 auto;
padding: 0 2rem 2rem;
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 2rem;
}
.card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
}
.card h2 {
color: #4a5568;
font-size: 1.5rem;
margin-bottom: 1.5rem;
border-bottom: 2px solid #e2e8f0;
padding-bottom: 0.5rem;
}
.metric-grid, .health-grid, .process-grid, .metrics-grid {
display: grid;
gap: 1rem;
}
.metric-item, .health-item, .process-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: rgba(247, 250, 252, 0.8);
border-radius: 8px;
border-left: 4px solid #3b82f6;
}
.metric-item strong, .health-item strong, .process-item strong {
color: #374151;
font-weight: 600;
}
.metric-value, .health-value, .process-value {
color: #6b7280;
font-family: 'Courier New', monospace;
font-weight: 500;
}
.memory-bars {
display: flex;
flex-direction: column;
gap: 1rem;
}
.memory-bar {
background: #e5e7eb;
border-radius: 10px;
overflow: hidden;
height: 30px;
position: relative;
}
.memory-bar-fill {
height: 100%;
background: linear-gradient(90deg, #10b981, #059669);
border-radius: 10px;
transition: width 0.5s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 0.9rem;
}
.memory-bar-fill.warning {
background: linear-gradient(90deg, #f59e0b, #d97706);
}
.memory-bar-fill.danger {
background: linear-gradient(90deg, #ef4444, #dc2626);
}
.memory-label {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-weight: 600;
color: #374151;
}
.actions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 1rem;
}
.btn {
padding: 0.75rem 1rem;
border: none;
border-radius: 8px;
font-weight: 600;
text-decoration: none;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
display: inline-block;
}
.btn-primary {
background: linear-gradient(135deg, #3b82f6, #2563eb);
color: white;
}
.btn-primary:hover {
background: linear-gradient(135deg, #2563eb, #1d4ed8);
transform: translateY(-2px);
}
.btn-secondary {
background: linear-gradient(135deg, #6b7280, #4b5563);
color: white;
}
.btn-secondary:hover {
background: linear-gradient(135deg, #4b5563, #374151);
transform: translateY(-2px);
}
.btn-info {
background: linear-gradient(135deg, #10b981, #059669);
color: white;
}
.btn-info:hover {
background: linear-gradient(135deg, #059669, #047857);
transform: translateY(-2px);
}
.loading {
text-align: center;
color: #9ca3af;
font-style: italic;
padding: 2rem;
}
.error {
background: rgba(239, 68, 68, 0.1);
color: #dc2626;
padding: 1rem;
border-radius: 8px;
border-left: 4px solid #ef4444;
}
footer {
text-align: center;
padding: 2rem;
color: rgba(255, 255, 255, 0.8);
background: rgba(0, 0, 0, 0.1);
margin-top: 2rem;
}
@media (max-width: 768px) {
.dashboard-grid {
grid-template-columns: 1fr;
}
header {
padding: 1rem;
}
header h1 {
font-size: 2rem;
}
main {
padding: 0 1rem 1rem;
}
.card {
padding: 1.5rem;
}
.actions-grid {
grid-template-columns: 1fr;
}
}

139
src/routes/api.js Normal file
View File

@ -0,0 +1,139 @@
const express = require('express');
const os = require('os');
const { getMetrics, incrementCounter } = require('../utils/metrics');
const router = express.Router();
// System information endpoint
router.get('/system', (req, res) => {
try {
incrementCounter('api_calls_total', { endpoint: '/system' });
const systemInfo = {
hostname: os.hostname(),
platform: os.platform(),
architecture: os.arch(),
cpus: os.cpus().length,
totalMemory: Math.round(os.totalmem() / 1024 / 1024 / 1024 * 100) / 100, // GB
freeMemory: Math.round(os.freemem() / 1024 / 1024 / 1024 * 100) / 100, // GB
uptime: Math.round(os.uptime()),
loadAverage: os.loadavg(),
networkInterfaces: Object.keys(os.networkInterfaces()),
nodeVersion: process.version,
timestamp: new Date().toISOString()
};
res.json({
success: true,
data: systemInfo
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch system information',
message: error.message
});
}
});
// Memory usage endpoint
router.get('/memory', (req, res) => {
try {
incrementCounter('api_calls_total', { endpoint: '/memory' });
const memoryUsage = process.memoryUsage();
const totalMem = os.totalmem();
const freeMem = os.freemem();
const usedMem = totalMem - freeMem;
const memoryInfo = {
system: {
total: Math.round(totalMem / 1024 / 1024 / 1024 * 100) / 100, // GB
used: Math.round(usedMem / 1024 / 1024 / 1024 * 100) / 100, // GB
free: Math.round(freeMem / 1024 / 1024 / 1024 * 100) / 100, // GB
percentage: Math.round((usedMem / totalMem) * 100)
},
process: {
rss: Math.round(memoryUsage.rss / 1024 / 1024 * 100) / 100, // MB
heapTotal: Math.round(memoryUsage.heapTotal / 1024 / 1024 * 100) / 100, // MB
heapUsed: Math.round(memoryUsage.heapUsed / 1024 / 1024 * 100) / 100, // MB
external: Math.round(memoryUsage.external / 1024 / 1024 * 100) / 100 // MB
},
timestamp: new Date().toISOString()
};
res.json({
success: true,
data: memoryInfo
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch memory information',
message: error.message
});
}
});
// Process information endpoint
router.get('/process', (req, res) => {
try {
incrementCounter('api_calls_total', { endpoint: '/process' });
const processInfo = {
pid: process.pid,
ppid: process.ppid,
uptime: Math.round(process.uptime()),
cwd: process.cwd(),
execPath: process.execPath,
version: process.version,
versions: process.versions,
env: process.env.NODE_ENV || 'development',
timestamp: new Date().toISOString()
};
res.json({
success: true,
data: processInfo
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch process information',
message: error.message
});
}
});
// Metrics endpoint
router.get('/metrics', (req, res) => {
try {
const metrics = getMetrics();
res.json({
success: true,
data: metrics,
timestamp: new Date().toISOString()
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch metrics',
message: error.message
});
}
});
// Test endpoint for CI/CD validation
router.get('/test', (req, res) => {
incrementCounter('api_calls_total', { endpoint: '/test' });
res.json({
success: true,
message: 'API is working correctly',
version: process.env.npm_package_version || '1.0.0',
environment: process.env.NODE_ENV || 'development',
timestamp: new Date().toISOString()
});
});
module.exports = router;

93
src/routes/health.js Normal file
View File

@ -0,0 +1,93 @@
const express = require('express');
const os = require('os');
const router = express.Router();
// Basic health check
router.get('/', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
service: 'harbor-ci-cd-demo'
});
});
// Detailed health check
router.get('/detailed', (req, res) => {
const memoryUsage = process.memoryUsage();
const totalMem = os.totalmem();
const freeMem = os.freemem();
// Check memory usage (warn if over 80%)
const memoryPercentage = ((totalMem - freeMem) / totalMem) * 100;
const memoryStatus = memoryPercentage > 80 ? 'warning' : 'healthy';
// Check process memory (warn if heap over 100MB)
const heapUsedMB = memoryUsage.heapUsed / 1024 / 1024;
const processMemoryStatus = heapUsedMB > 100 ? 'warning' : 'healthy';
// Overall status
const overallStatus = (memoryStatus === 'warning' || processMemoryStatus === 'warning')
? 'warning'
: 'healthy';
res.json({
status: overallStatus,
timestamp: new Date().toISOString(),
service: 'harbor-ci-cd-demo',
version: process.env.npm_package_version || '1.0.0',
uptime: {
process: Math.round(process.uptime()),
system: Math.round(os.uptime())
},
memory: {
status: memoryStatus,
system: {
total: Math.round(totalMem / 1024 / 1024 / 1024 * 100) / 100,
used: Math.round((totalMem - freeMem) / 1024 / 1024 / 1024 * 100) / 100,
percentage: Math.round(memoryPercentage)
},
process: {
status: processMemoryStatus,
heapUsed: Math.round(heapUsedMB),
heapTotal: Math.round(memoryUsage.heapTotal / 1024 / 1024),
rss: Math.round(memoryUsage.rss / 1024 / 1024)
}
},
loadAverage: os.loadavg().map(load => Math.round(load * 100) / 100),
environment: process.env.NODE_ENV || 'development'
});
});
// Readiness probe (for Kubernetes-style deployments)
router.get('/ready', (req, res) => {
// Check if the application is ready to serve traffic
// In a real app, you might check database connections, external services, etc.
const isReady = true; // Simplified check
if (isReady) {
res.json({
status: 'ready',
timestamp: new Date().toISOString()
});
} else {
res.status(503).json({
status: 'not ready',
timestamp: new Date().toISOString()
});
}
});
// Liveness probe (for Kubernetes-style deployments)
router.get('/live', (req, res) => {
// Check if the application is alive and should not be restarted
res.json({
status: 'alive',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
module.exports = router;

95
src/utils/metrics.js Normal file
View File

@ -0,0 +1,95 @@
// Simple in-memory metrics storage
// In production, you'd use Prometheus, StatsD, or similar
const metrics = {
counters: {},
gauges: {},
histograms: {},
startTime: Date.now()
};
// Increment a counter
function incrementCounter(name, labels = {}) {
const key = `${name}_${JSON.stringify(labels)}`;
if (!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,
labels,
value,
timestamp: Date.now()
};
}
// Record histogram value (simplified)
function recordHistogram(name, value, labels = {}) {
const key = `${name}_${JSON.stringify(labels)}`;
if (!metrics.histograms[key]) {
metrics.histograms[key] = {
name,
labels,
values: [],
count: 0,
sum: 0
};
}
const histogram = metrics.histograms[key];
histogram.values.push(value);
histogram.count++;
histogram.sum += value;
// Keep only last 1000 values to prevent memory issues
if (histogram.values.length > 1000) {
histogram.values = histogram.values.slice(-1000);
}
}
// 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
function resetMetrics() {
metrics.counters = {};
metrics.gauges = {};
metrics.histograms = {};
metrics.startTime = Date.now();
}
module.exports = {
incrementCounter,
setGauge,
recordHistogram,
getMetrics,
resetMetrics
};

87
tests/api.tests.js Normal file
View File

@ -0,0 +1,87 @@
const request = require('supertest');
const app = require('../src/app');
describe('API Endpoints', () => {
describe('GET /api/system', () => {
it('should return system information', async () => {
const response = await request(app)
.get('/api/system')
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('hostname');
expect(response.body.data).toHaveProperty('platform');
expect(response.body.data).toHaveProperty('cpus');
expect(response.body.data).toHaveProperty('totalMemory');
expect(typeof response.body.data.cpus).toBe('number');
});
});
describe('GET /api/memory', () => {
it('should return memory information', async () => {
const response = await request(app)
.get('/api/memory')
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('system');
expect(response.body.data).toHaveProperty('process');
expect(response.body.data.system).toHaveProperty('total');
expect(response.body.data.system).toHaveProperty('used');
expect(response.body.data.process).toHaveProperty('rss');
});
});
describe('GET /api/process', () => {
it('should return process information', async () => {
const response = await request(app)
.get('/api/process')
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('pid');
expect(response.body.data).toHaveProperty('uptime');
expect(response.body.data).toHaveProperty('version');
expect(typeof response.body.data.pid).toBe('number');
});
});
describe('GET /api/metrics', () => {
it('should return application metrics', async () => {
const response = await request(app)
.get('/api/metrics')
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('counters');
expect(response.body.data).toHaveProperty('gauges');
expect(response.body.data).toHaveProperty('histograms');
expect(response.body.data).toHaveProperty('runtime');
expect(Array.isArray(response.body.data.counters)).toBe(true);
});
});
describe('GET /api/test', () => {
it('should return test response', async () => {
const response = await request(app)
.get('/api/test')
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.message).toBe('API is working correctly');
expect(response.body).toHaveProperty('version');
expect(response.body).toHaveProperty('timestamp');
});
});
describe('Error handling', () => {
it('should return 404 for unknown API endpoints', async () => {
const response = await request(app)
.get('/api/nonexistent')
.expect(404);
expect(response.body).toHaveProperty('error');
expect(response.body.error).toBe('Not Found');
});
});
});

103
tests/health.tests.js Normal file
View File

@ -0,0 +1,103 @@
const request = require('supertest');
const app = require('../src/app');
describe('Health Check Endpoints', () => {
describe('GET /health', () => {
it('should return basic health status', async () => {
const response = await request(app)
.get('/health')
.expect(200);
expect(response.body).toHaveProperty('status', 'healthy');
expect(response.body).toHaveProperty('timestamp');
expect(response.body).toHaveProperty('uptime');
expect(response.body).toHaveProperty('service', 'harbor-ci-cd-demo');
expect(typeof response.body.uptime).toBe('number');
});
});
describe('GET /health/detailed', () => {
it('should return detailed health information', async () => {
const response = await request(app)
.get('/health/detailed')
.expect(200);
expect(response.body).toHaveProperty('status');
expect(response.body).toHaveProperty('timestamp');
expect(response.body).toHaveProperty('service', 'harbor-ci-cd-demo');
expect(response.body).toHaveProperty('uptime');
expect(response.body).toHaveProperty('memory');
expect(response.body).toHaveProperty('loadAverage');
expect(response.body).toHaveProperty('environment');
// Check memory structure
expect(response.body.memory).toHaveProperty('status');
expect(response.body.memory).toHaveProperty('system');
expect(response.body.memory).toHaveProperty('process');
expect(response.body.memory.system).toHaveProperty('total');
expect(response.body.memory.system).toHaveProperty('used');
expect(response.body.memory.system).toHaveProperty('percentage');
// Check uptime structure
expect(response.body.uptime).toHaveProperty('process');
expect(response.body.uptime).toHaveProperty('system');
expect(typeof response.body.uptime.process).toBe('number');
expect(typeof response.body.uptime.system).toBe('number');
// Check load average
expect(Array.isArray(response.body.loadAverage)).toBe(true);
expect(response.body.loadAverage).toHaveLength(3);
});
it('should include version information', async () => {
const response = await request(app)
.get('/health/detailed')
.expect(200);
expect(response.body).toHaveProperty('version');
});
});
describe('GET /health/ready', () => {
it('should return readiness status', async () => {
const response = await request(app)
.get('/health/ready')
.expect(200);
expect(response.body).toHaveProperty('status', 'ready');
expect(response.body).toHaveProperty('timestamp');
});
});
describe('GET /health/live', () => {
it('should return liveness status', async () => {
const response = await request(app)
.get('/health/live')
.expect(200);
expect(response.body).toHaveProperty('status', 'alive');
expect(response.body).toHaveProperty('timestamp');
expect(response.body).toHaveProperty('uptime');
expect(typeof response.body.uptime).toBe('number');
});
});
describe('Health status validation', () => {
it('should return valid timestamp format', async () => {
const response = await request(app)
.get('/health')
.expect(200);
const timestamp = new Date(response.body.timestamp);
expect(timestamp).toBeInstanceOf(Date);
expect(timestamp.getTime()).not.toBeNaN();
});
it('should return consistent service name across endpoints', async () => {
const basicHealth = await request(app).get('/health');
const detailedHealth = await request(app).get('/health/detailed');
expect(basicHealth.body.service).toBe(detailedHealth.body.service);
});
});
});