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
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:
34
.eslintrc.json
Normal file
34
.eslintrc.json
Normal 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
236
.gitea/workflows/ci-cd.yml
Normal 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
32
Dockerfile
Normal 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"]
|
||||||
288
README.md
288
README.md
@ -1,3 +1,289 @@
|
|||||||
# harbor-ci-cd-demo
|
# harbor-ci-cd-demo
|
||||||
|
|
||||||
Learning Harbor + Gitea Actions CI/CD integration
|
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
43
docker-compose.yml
Normal 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
6081
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
47
package.json
Normal file
47
package.json
Normal 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
139
src/app.js
Normal 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
43
src/middleware/logger.js
Normal 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
422
src/public/app.js
Normal 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
81
src/public/index.html
Normal 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
276
src/public/style.css
Normal 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
139
src/routes/api.js
Normal 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
93
src/routes/health.js
Normal 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
95
src/utils/metrics.js
Normal 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
87
tests/api.tests.js
Normal 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
103
tests/health.tests.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user