diff --git a/monitoring/grafana-values.yaml b/monitoring/grafana-values.yaml new file mode 100644 index 0000000..71da07f --- /dev/null +++ b/monitoring/grafana-values.yaml @@ -0,0 +1,61 @@ +adminUser: admin +adminPassword: admin1205 + +persistence: + enabled: true + size: 10Gi + +service: + type: NodePort + nodePort: 30030 + +resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi + +datasources: + datasources.yaml: + apiVersion: 1 + datasources: + - name: Prometheus + type: prometheus + url: http://prometheus-server.monitoring.svc.cluster.local + access: proxy + isDefault: true + editable: true + +dashboardProviders: + dashboardproviders.yaml: + apiVersion: 1 + providers: + - name: 'default' + orgId: 1 + folder: '' + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards/default + +dashboards: + default: + kubernetes-cluster: + gnetId: 7249 + revision: 1 + datasource: Prometheus + kubernetes-pods: + gnetId: 6417 + revision: 1 + datasource: Prometheus + node-exporter: + gnetId: 1860 + revision: 27 + datasource: Prometheus + +plugins: + - grafana-piechart-panel + - grafana-clock-panel diff --git a/monitoring/prometheus-values.yaml b/monitoring/prometheus-values.yaml new file mode 100644 index 0000000..f741307 --- /dev/null +++ b/monitoring/prometheus-values.yaml @@ -0,0 +1,69 @@ +server: + persistentVolume: + enabled: true + size: 8Gi + + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 250m + memory: 256Mi + + retention: "15d" + + service: + type: NodePort + nodePort: 30090 + + global: + scrape_interval: 15s + scrape_timeout: 10s + evaluation_interval: 15s + +alertmanager: + enabled: true + persistentVolume: + enabled: true + size: 2Gi + + service: + type: NodePort + nodePort: 30093 + +pushgateway: + enabled: false + +nodeExporter: + enabled: true + +kubeStateMetrics: + enabled: true + +serviceMonitors: + enabled: true + +serverFiles: + prometheus.yml: + scrape_configs: + - job_name: 'quote-app' + kubernetes_sd_configs: + - role: endpoints + namespaces: + names: + - quote-app + - quote-app-terraform + relabel_configs: + - source_labels: [__meta_kubernetes_service_label_app] + action: keep + regex: quote-app + - source_labels: [__meta_kubernetes_endpoint_port_name] + action: keep + regex: http + - source_labels: [__meta_kubernetes_namespace] + target_label: namespace + - source_labels: [__meta_kubernetes_pod_name] + target_label: pod + - source_labels: [__meta_kubernetes_service_name] + target_label: service diff --git a/monitoring/quote-app-dashboard.json b/monitoring/quote-app-dashboard.json new file mode 100644 index 0000000..a621081 --- /dev/null +++ b/monitoring/quote-app-dashboard.json @@ -0,0 +1,120 @@ +{ + "dashboard": { + "title": "Quote App Metrics", + "tags": ["quote-app", "nodejs"], + "timezone": "browser", + "panels": [ + { + "id": 1, + "title": "Total Quotes Served", + "type": "stat", + "targets": [ + { + "expr": "sum(quotes_served_total)", + "refId": "A" + } + ], + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 0 + } + }, + { + "id": 2, + "title": "HTTP Request Rate", + "type": "graph", + "targets": [ + { + "expr": "rate(http_requests_total[5m])", + "legendFormat": "{{method}} {{route}}", + "refId": "A" + } + ], + "gridPos": { + "h": 8, + "w": 18, + "x": 6, + "y": 0 + } + }, + { + "id": 3, + "title": "Request Duration (p95)", + "type": "graph", + "targets": [ + { + "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))", + "legendFormat": "{{route}}", + "refId": "A" + } + ], + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + } + }, + { + "id": 4, + "title": "Active Pods", + "type": "stat", + "targets": [ + { + "expr": "count(kube_pod_info{namespace=\"quote-app\"})", + "refId": "A" + } + ], + "gridPos": { + "h": 8, + "w": 6, + "x": 12, + "y": 8 + } + }, + { + "id": 5, + "title": "CPU Usage", + "type": "graph", + "targets": [ + { + "expr": "rate(container_cpu_usage_seconds_total{namespace=\"quote-app\"}[5m])", + "legendFormat": "{{pod}}", + "refId": "A" + } + ], + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + } + }, + { + "id": 6, + "title": "Memory Usage", + "type": "graph", + "targets": [ + { + "expr": "container_memory_usage_bytes{namespace=\"quote-app\"}", + "legendFormat": "{{pod}}", + "refId": "A" + } + ], + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + } + } + ], + "time": { + "from": "now-1h", + "to": "now" + }, + "refresh": "10s" + } +} diff --git a/monitoring/quote-app-servicemonitor.yaml b/monitoring/quote-app-servicemonitor.yaml new file mode 100644 index 0000000..953a707 --- /dev/null +++ b/monitoring/quote-app-servicemonitor.yaml @@ -0,0 +1,16 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: quote-app-monitor + namespace: quote-app + labels: + app: quote-app + release: prometheus +spec: + selector: + matchLabels: + app: quote-app + endpoints: + - port: http + path: /metrics + interval: 30s diff --git a/package-lock.json b/package-lock.json index b078aa6..936343a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "dependencies": { "cors": "^2.8.5", "dotenv": "^16.3.1", - "express": "^4.18.2" + "express": "^4.18.2", + "prom-client": "^15.1.3" }, "devDependencies": { "jest": "^29.7.0", @@ -897,6 +898,15 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@paralleldrive/cuid2": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", @@ -1295,6 +1305,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -3389,9 +3405,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -4017,6 +4033,19 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prom-client": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", + "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -4656,6 +4685,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "license": "MIT", + "dependencies": { + "bintrees": "1.0.2" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", diff --git a/package.json b/package.json index a5983cd..d2b333b 100644 --- a/package.json +++ b/package.json @@ -9,17 +9,23 @@ "test": "jest --coverage", "test:watch": "jest --watch" }, - "keywords": ["quotes", "api", "devops", "cicd"], + "keywords": [ + "quotes", + "api", + "devops", + "cicd" + ], "author": "Aditya Singh", "license": "MIT", "dependencies": { - "express": "^4.18.2", "cors": "^2.8.5", - "dotenv": "^16.3.1" + "dotenv": "^16.3.1", + "express": "^4.18.2", + "prom-client": "^15.1.3" }, "devDependencies": { - "nodemon": "^3.0.1", "jest": "^29.7.0", + "nodemon": "^3.0.1", "supertest": "^6.3.3" } } diff --git a/src/app.js b/src/app.js index d86e118..152eeb2 100644 --- a/src/app.js +++ b/src/app.js @@ -7,6 +7,36 @@ const cors = require('cors'); const path = require('path'); require('dotenv').config(); +// Prometheus metrics +const promClient = require('prom-client'); +const register = new promClient.Registry(); + +// Collect default metrics +promClient.collectDefaultMetrics({ register }); + +// Custom metrics +const httpRequestDuration = new promClient.Histogram({ + name: 'http_request_duration_seconds', + help: 'Duration of HTTP requests in seconds', + labelNames: ['method', 'route', 'status_code'], + buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10] +}); +register.registerMetric(httpRequestDuration); + +const httpRequestTotal = new promClient.Counter({ + name: 'http_requests_total', + help: 'Total number of HTTP requests', + labelNames: ['method', 'route', 'status_code'] +}); +register.registerMetric(httpRequestTotal); + +const quotesServedTotal = new promClient.Counter({ + name: 'quotes_served_total', + help: 'Total number of quotes served', + labelNames: ['endpoint'] +}); +register.registerMetric(quotesServedTotal); + // ============================================ // APP INITIALIZATION // ============================================ @@ -22,6 +52,19 @@ app.use(cors()); app.use(express.json()); app.use(express.static('public')); +// Metrics middleware +app.use((req, res, next) => { + const start = Date.now(); + + res.on('finish', () => { + const duration = (Date.now() - start) / 1000; + httpRequestDuration.labels(req.method, req.path, res.statusCode).observe(duration); + httpRequestTotal.labels(req.method, req.path, res.statusCode).inc(); + }); + + next(); +}); + // ============================================ // DATA LOADING // ============================================ @@ -32,6 +75,15 @@ const quotes = require('./data/quotes.json'); // API ROUTES // ============================================ +/** + * Metrics Endpoint for Prometheus + * GET /metrics + */ +app.get('/metrics', async (req, res) => { + res.setHeader('Content-Type', register.contentType); + res.send(await register.metrics()); +}); + /** * Health Check Endpoint * GET /health @@ -75,6 +127,10 @@ app.get('/api/quotes', (req, res) => { app.get('/api/quotes/random', (req, res) => { const randomIndex = Math.floor(Math.random() * quotes.length); const randomQuote = quotes[randomIndex]; + + // Track metric + quotesServedTotal.labels('/api/quotes/random').inc(); + res.json(randomQuote); });