diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index ad4eb54..6964f3d 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,22 +4,11 @@
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
@@ -96,7 +85,7 @@
-
+
@@ -196,10 +185,10 @@
+
-
@@ -252,7 +241,23 @@
1741363113389
-
+
+
+ 1741398732439
+
+
+
+ 1741398732439
+
+
+
+ 1741496590754
+
+
+
+ 1741496590754
+
+
@@ -275,7 +280,9 @@
-
+
+
+
diff --git a/Tiltfile b/Tiltfile
index 18c2012..750d885 100644
--- a/Tiltfile
+++ b/Tiltfile
@@ -2,11 +2,16 @@
k8s_yaml([
"kubernetes/infrastructure/keycloak/keycloak.yml",
"kubernetes/infrastructure/postgres/postgres.yml",
- "kubernetes/infrastructure/mongodb/mongodb.yml"
+ "kubernetes/infrastructure/mongodb/mongodb.yml",
+ "kubernetes/infrastructure/prometheus/prometheus.yml",
+ "kubernetes/infrastructure/grafana/grafana.yml"
+
])
# Define infrastructure resources
k8s_resource("keycloak", labels=["infra"], auto_init=True)
+k8s_resource("prometheus", labels=["infra"], auto_init=True)
+k8s_resource("grafana", labels=["infra"], auto_init=True)
k8s_resource("course-postgres", labels=["infra"], auto_init=True)
k8s_resource("review-mongodb", labels=["infra"], auto_init=True)
diff --git a/docker/dashboard-1.yml b/docker/dashboard-1.yml
new file mode 100644
index 0000000..155facc
--- /dev/null
+++ b/docker/dashboard-1.yml
@@ -0,0 +1,554 @@
+{
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": "-- Grafana --",
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": true,
+ "gnetId": null,
+ "graphTooltip": 0,
+ "id": null,
+ "links": [],
+ "panels": [
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "Prometheus",
+ "description": "Rate of API requests per second for /hello endpoint",
+ "fill": 1,
+ "fillGradient": 5,
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 },
+ "id": 1,
+ "legend": {
+ "avg": true,
+ "current": true,
+ "max": true,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": true
+ },
+ "lines": true,
+ "linewidth": 2,
+ "nullPointMode": "null",
+ "options": {
+ "alertThreshold": true
+ },
+ "percentage": false,
+ "pluginVersion": "10.4.0",
+ "pointradius": 2,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "rate(api_requests_total{application=\"course-composite-service\", endpoint=\"/hello\"}[5m])",
+ "legendFormat": "Requests/sec - /hello",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "API Request Rate",
+ "tooltip": { "shared": true, "sort": 0, "value_type": "individual" },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "reqps",
+ "label": "Requests/sec",
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": { "align": false, "alignLevel": null }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "Prometheus",
+ "description": "API request latency for /hello endpoint",
+ "fill": 1,
+ "fillGradient": 3,
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 },
+ "id": 2,
+ "legend": {
+ "avg": true,
+ "current": true,
+ "max": true,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": true
+ },
+ "lines": true,
+ "linewidth": 2,
+ "nullPointMode": "null",
+ "options": {
+ "alertThreshold": true
+ },
+ "percentage": false,
+ "pluginVersion": "10.4.0",
+ "pointradius": 2,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "api_request_duration_seconds_max{application=\"course-composite-service\", endpoint=\"/hello\"}",
+ "legendFormat": "Max Latency - /hello",
+ "refId": "A"
+ },
+ {
+ "expr": "api_request_duration_seconds_sum{application=\"course-composite-service\", endpoint=\"/hello\"} / api_request_duration_seconds_count{application=\"course-composite-service\", endpoint=\"/hello\"}",
+ "legendFormat": "Avg Latency - /hello",
+ "refId": "B"
+ }
+ ],
+ "thresholds": [
+ {
+ "colorMode": "critical",
+ "fill": true,
+ "line": true,
+ "op": "gt",
+ "value": 1,
+ "yaxis": "left"
+ }
+ ],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "API Latency",
+ "tooltip": { "shared": true, "sort": 0, "value_type": "individual" },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "s",
+ "label": "Seconds",
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": { "align": false, "alignLevel": null }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "Prometheus",
+ "description": "JVM heap memory usage",
+ "fill": 1,
+ "fillGradient": 7,
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 },
+ "id": 3,
+ "legend": {
+ "avg": true,
+ "current": true,
+ "max": true,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": true
+ },
+ "lines": true,
+ "linewidth": 2,
+ "nullPointMode": "null",
+ "options": {
+ "alertThreshold": true
+ },
+ "percentage": false,
+ "pluginVersion": "10.4.0",
+ "pointradius": 2,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "jvm_memory_used_bytes{application=\"course-composite-service\", area=\"heap\", id=\"G1 Eden Space\"}",
+ "legendFormat": "Eden Used",
+ "refId": "A"
+ },
+ {
+ "expr": "jvm_memory_used_bytes{application=\"course-composite-service\", area=\"heap\", id=\"G1 Old Gen\"}",
+ "legendFormat": "Old Gen Used",
+ "refId": "B"
+ },
+ {
+ "expr": "jvm_memory_used_bytes{application=\"course-composite-service\", area=\"heap\", id=\"G1 Survivor Space\"}",
+ "legendFormat": "Survivor Used",
+ "refId": "C"
+ },
+ {
+ "expr": "jvm_memory_max_bytes{application=\"course-composite-service\", area=\"heap\", id=\"G1 Old Gen\"}",
+ "legendFormat": "Old Gen Max",
+ "refId": "D"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "JVM Heap Memory Usage",
+ "tooltip": { "shared": true, "sort": 0, "value_type": "individual" },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "bytes",
+ "label": "Memory",
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": { "align": false, "alignLevel": null }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "Prometheus",
+ "description": "JVM garbage collection pause time",
+ "fill": 1,
+ "fillGradient": 5,
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 },
+ "id": 4,
+ "legend": {
+ "avg": true,
+ "current": true,
+ "max": true,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": true
+ },
+ "lines": true,
+ "linewidth": 2,
+ "nullPointMode": "null",
+ "options": {
+ "alertThreshold": true
+ },
+ "percentage": false,
+ "pluginVersion": "10.4.0",
+ "pointradius": 2,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "jvm_gc_pause_seconds_max{application=\"course-composite-service\", gc=\"G1 Young Generation\"}",
+ "legendFormat": "Max GC Pause",
+ "refId": "A"
+ },
+ {
+ "expr": "jvm_gc_pause_seconds_sum{application=\"course-composite-service\", gc=\"G1 Young Generation\"} / jvm_gc_pause_seconds_count{application=\"course-composite-service\", gc=\"G1 Young Generation\"}",
+ "legendFormat": "Avg GC Pause",
+ "refId": "B"
+ }
+ ],
+ "thresholds": [
+ {
+ "colorMode": "critical",
+ "fill": true,
+ "line": true,
+ "op": "gt",
+ "value": 0.5,
+ "yaxis": "left"
+ }
+ ],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "JVM GC Pause Time",
+ "tooltip": { "shared": true, "sort": 0, "value_type": "individual" },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "s",
+ "label": "Seconds",
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": { "align": false, "alignLevel": null }
+ },
+ {
+ "cacheTimeout": null,
+ "colorBackground": false,
+ "colorValue": true,
+ "colors": ["#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a"],
+ "datasource": "Prometheus",
+ "description": "JVM process CPU usage",
+ "format": "percent",
+ "gauge": {
+ "maxValue": 100,
+ "minValue": 0,
+ "show": true,
+ "thresholdLabels": false,
+ "thresholdMarkers": true
+ },
+ "gridPos": { "h": 4, "w": 8, "x": 0, "y": 16 },
+ "id": 5,
+ "interval": null,
+ "links": [],
+ "mappingType": 1,
+ "mappingTypes": [
+ { "name": "value to text", "value": 1 },
+ { "name": "range to text", "value": 2 }
+ ],
+ "maxDataPoints": 100,
+ "nullPointMode": "connected",
+ "nullText": null,
+ "postfix": "%",
+ "postfixFontSize": "50%",
+ "prefix": "",
+ "prefixFontSize": "50%",
+ "rangeMaps": [{ "from": "null", "text": "N/A", "to": "null" }],
+ "sparkline": {
+ "fillColor": "rgba(31, 118, 189, 0.18)",
+ "full": false,
+ "lineColor": "#3274D9",
+ "show": true,
+ "ymax": null,
+ "ymin": null
+ },
+ "tableColumn": "",
+ "targets": [
+ {
+ "expr": "process_cpu_usage{application=\"course-composite-service\"} * 100",
+ "legendFormat": "",
+ "refId": "A"
+ }
+ ],
+ "thresholds": "70,90",
+ "title": "JVM CPU Usage",
+ "type": "singlestat",
+ "valueFontSize": "80%",
+ "valueMaps": [{ "op": "=", "text": "N/A", "value": "null" }],
+ "valueName": "current"
+ },
+ {
+ "cacheTimeout": null,
+ "colorBackground": false,
+ "colorValue": true,
+ "colors": ["#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a"],
+ "datasource": "Prometheus",
+ "description": "System CPU usage",
+ "format": "percent",
+ "gauge": {
+ "maxValue": 100,
+ "minValue": 0,
+ "show": true,
+ "thresholdLabels": false,
+ "thresholdMarkers": true
+ },
+ "gridPos": { "h": 4, "w": 8, "x": 8, "y": 16 },
+ "id": 6,
+ "interval": null,
+ "links": [],
+ "mappingType": 1,
+ "mappingTypes": [
+ { "name": "value to text", "value": 1 },
+ { "name": "range to text", "value": 2 }
+ ],
+ "maxDataPoints": 100,
+ "nullPointMode": "connected",
+ "nullText": null,
+ "postfix": "%",
+ "postfixFontSize": "50%",
+ "prefix": "",
+ "prefixFontSize": "50%",
+ "rangeMaps": [{ "from": "null", "text": "N/A", "to": "null" }],
+ "sparkline": {
+ "fillColor": "rgba(31, 118, 189, 0.18)",
+ "full": false,
+ "lineColor": "#3274D9",
+ "show": true,
+ "ymax": null,
+ "ymin": null
+ },
+ "tableColumn": "",
+ "targets": [
+ {
+ "expr": "system_cpu_usage{application=\"course-composite-service\"} * 100",
+ "legendFormat": "",
+ "refId": "A"
+ }
+ ],
+ "thresholds": "70,90",
+ "title": "System CPU Usage",
+ "type": "singlestat",
+ "valueFontSize": "80%",
+ "valueMaps": [{ "op": "=", "text": "N/A", "value": "null" }],
+ "valueName": "current"
+ },
+ {
+ "cacheTimeout": null,
+ "colorBackground": false,
+ "colorValue": true,
+ "colors": ["#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a"],
+ "datasource": "Prometheus",
+ "description": "System load average over 1 minute",
+ "format": "short",
+ "gauge": {
+ "maxValue": 12,
+ "minValue": 0,
+ "show": true,
+ "thresholdLabels": false,
+ "thresholdMarkers": true
+ },
+ "gridPos": { "h": 4, "w": 8, "x": 16, "y": 16 },
+ "id": 7,
+ "interval": null,
+ "links": [],
+ "mappingType": 1,
+ "mappingTypes": [
+ { "name": "value to text", "value": 1 },
+ { "name": "range to text", "value": 2 }
+ ],
+ "maxDataPoints": 100,
+ "nullPointMode": "connected",
+ "nullText": null,
+ "postfix": "",
+ "postfixFontSize": "50%",
+ "prefix": "",
+ "prefixFontSize": "50%",
+ "rangeMaps": [{ "from": "null", "text": "N/A", "to": "null" }],
+ "sparkline": {
+ "fillColor": "rgba(31, 118, 189, 0.18)",
+ "full": false,
+ "lineColor": "#3274D9",
+ "show": true,
+ "ymax": null,
+ "ymin": null
+ },
+ "tableColumn": "",
+ "targets": [
+ {
+ "expr": "system_load_average_1m{application=\"course-composite-service\"}",
+ "legendFormat": "",
+ "refId": "A"
+ }
+ ],
+ "thresholds": "8,10",
+ "title": "System Load (1m)",
+ "type": "singlestat",
+ "valueFontSize": "80%",
+ "valueMaps": [{ "op": "=", "text": "N/A", "value": "null" }],
+ "valueName": "current"
+ }
+ ],
+ "refresh": "5s",
+ "schemaVersion": 36,
+ "style": "dark",
+ "tags": ["spring-boot", "api", "jvm", "cpu"],
+ "templating": {
+ "list": []
+ },
+ "time": {
+ "from": "now-15m",
+ "to": "now"
+ },
+ "timepicker": {
+ "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h"],
+ "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
+ },
+ "timezone": "",
+ "title": "Spring Boot API, JVM, and CPU Dashboard",
+ "uid": null,
+ "version": 1,
+ "weekStart": ""
+}
\ No newline at end of file
diff --git a/docker/docker-compose-base.yml b/docker/docker-compose-base.yml
index df40e30..e5e8d3c 100644
--- a/docker/docker-compose-base.yml
+++ b/docker/docker-compose-base.yml
@@ -43,4 +43,4 @@ services:
networks:
shared-network:
- name: shared-network
\ No newline at end of file
+ driver: bridge
\ No newline at end of file
diff --git a/docker/docker-compose-infra.yml b/docker/docker-compose-infra.yml
index 24e4724..d328dc0 100644
--- a/docker/docker-compose-infra.yml
+++ b/docker/docker-compose-infra.yml
@@ -42,6 +42,36 @@ services:
command: [ "start-dev" ]
networks:
- shared-network
+
+ prometheus:
+ image: prom/prometheus:latest
+ volumes:
+ - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
+ - prometheus-data:/prometheus
+ command:
+ - '--config.file=/etc/prometheus/prometheus.yml'
+ - '--storage.tsdb.path=/prometheus'
+ ports:
+ - "9090:9090"
+ networks:
+ - shared-network
+
+ grafana:
+ image: grafana/grafana:latest
+ volumes:
+ - grafana-data:/var/lib/grafana
+ ports:
+ - "3000:3000"
+ environment:
+ - GF_SECURITY_ADMIN_PASSWORD=admin
+ depends_on:
+ - prometheus
+ networks:
+ - shared-network
+
networks:
shared-network:
- name: shared-network
\ No newline at end of file
+ driver: bridge
+volumes:
+ prometheus-data:
+ grafana-data:
\ No newline at end of file
diff --git a/docker/prometheus/prometheus.yml b/docker/prometheus/prometheus.yml
new file mode 100644
index 0000000..485858d
--- /dev/null
+++ b/docker/prometheus/prometheus.yml
@@ -0,0 +1,11 @@
+global:
+ scrape_interval: 15s
+scrape_configs:
+ - job_name: 'course-composite-app'
+ metrics_path: '/actuator/prometheus'
+ static_configs:
+ - targets:
+ - 'course-composite:8080'
+ - 'course:8080'
+ - 'review:8080'
+ - 'gateway-service:9000'
\ No newline at end of file
diff --git a/kubernetes/infrastructure/grafana/grafana.yml b/kubernetes/infrastructure/grafana/grafana.yml
new file mode 100644
index 0000000..f71d90f
--- /dev/null
+++ b/kubernetes/infrastructure/grafana/grafana.yml
@@ -0,0 +1,56 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: grafana
+ labels:
+ app: grafana
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: grafana
+ template:
+ metadata:
+ labels:
+ app: grafana
+ spec:
+ containers:
+ - name: grafana
+ image: grafana/grafana:latest
+ ports:
+ - containerPort: 3000
+ env:
+ - name: GF_SECURITY_ADMIN_PASSWORD
+ value: "admin"
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: grafana
+ labels:
+ app: grafana
+spec:
+ selector:
+ app: grafana
+ ports:
+ - protocol: TCP
+ port: 3000
+ targetPort: 3000
+ type: ClusterIP
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: grafana-ingress
+spec:
+ rules:
+ - host: grafana.local
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: grafana
+ port:
+ number: 3000
diff --git a/kubernetes/infrastructure/prometheus/prometheus.yml b/kubernetes/infrastructure/prometheus/prometheus.yml
new file mode 100644
index 0000000..d11823d
--- /dev/null
+++ b/kubernetes/infrastructure/prometheus/prometheus.yml
@@ -0,0 +1,76 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: prometheus-config
+data:
+ prometheus.yml: |
+ global:
+ scrape_interval: 15s
+ scrape_configs:
+ - job_name: 'spring-boot-app'
+ metrics_path: '/actuator/prometheus'
+ static_configs:
+ - targets:
+ - 'course-composite-service:80'
+ - 'course-service:80'
+ - 'review-service:80'
+ - 'gateway-service:80'
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: prometheus
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: prometheus
+ template:
+ metadata:
+ labels:
+ app: prometheus
+ spec:
+ containers:
+ - name: prometheus
+ image: prom/prometheus:latest
+ args:
+ - "--config.file=/etc/prometheus/prometheus.yml"
+ ports:
+ - containerPort: 9090
+ volumeMounts:
+ - name: config-volume
+ mountPath: "/etc/prometheus/"
+ volumes:
+ - name: config-volume
+ configMap:
+ name: prometheus-config
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: prometheus
+spec:
+ selector:
+ app: prometheus
+ ports:
+ - port: 9090
+ targetPort: 9090
+ protocol: TCP
+ type: ClusterIP
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: prometheus-ingress
+spec:
+ rules:
+ - host: prometheus.local
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: prometheus
+ port:
+ number: 9090
\ No newline at end of file
diff --git a/microservices/course-composite-service/pom.xml b/microservices/course-composite-service/pom.xml
index c6d2224..12460a1 100644
--- a/microservices/course-composite-service/pom.xml
+++ b/microservices/course-composite-service/pom.xml
@@ -61,6 +61,12 @@
provided
+
+ io.micrometer
+ micrometer-registry-prometheus
+
+
+
org.springframework.boot
spring-boot-starter-test
diff --git a/microservices/course-composite-service/src/main/java/io/javatab/microservices/composite/course/MetricsController.java b/microservices/course-composite-service/src/main/java/io/javatab/microservices/composite/course/MetricsController.java
new file mode 100644
index 0000000..9126c34
--- /dev/null
+++ b/microservices/course-composite-service/src/main/java/io/javatab/microservices/composite/course/MetricsController.java
@@ -0,0 +1,59 @@
+package io.javatab.microservices.composite.course;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Timer;
+import jakarta.annotation.PostConstruct;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+
+import java.util.Random;
+
+@RestController
+@RequestMapping("/api/metrics")
+public class MetricsController {
+
+ private final MeterRegistry meterRegistry;
+ private Counter requestCounter;
+ private Timer requestTimer;
+
+ public MetricsController(MeterRegistry meterRegistry) {
+ this.meterRegistry = meterRegistry;
+ }
+
+ @PostConstruct
+ public void init() {
+ // Initialize custom metrics
+ requestCounter = Counter
+ .builder("api.requests.total")
+ .description("Total number of API requests")
+ .tags("endpoint", "/hello")
+ .register(meterRegistry);
+
+ requestTimer = Timer
+ .builder("api.request.duration")
+ .description("Time taken to process requests")
+ .tags("endpoint", "/hello")
+ .register(meterRegistry);
+ }
+
+ @GetMapping("/hello")
+ public String hello() {
+ // Record request count
+ requestCounter.increment();
+
+ // Record execution time
+ return requestTimer.record(() -> {
+ try {
+ // Simulate some work
+ int sleepTime = new Random().nextInt(1000);
+ Thread.sleep(sleepTime);
+ return "Hello, World!";
+ } catch (InterruptedException e) {
+ return "Error occurred";
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/microservices/course-composite-service/src/main/java/io/javatab/microservices/composite/course/config/MetricsConfig.java b/microservices/course-composite-service/src/main/java/io/javatab/microservices/composite/course/config/MetricsConfig.java
new file mode 100644
index 0000000..daf3373
--- /dev/null
+++ b/microservices/course-composite-service/src/main/java/io/javatab/microservices/composite/course/config/MetricsConfig.java
@@ -0,0 +1,32 @@
+package io.javatab.microservices.composite.course.config;
+
+import io.micrometer.core.instrument.Gauge;
+import io.micrometer.core.instrument.MeterRegistry;
+import jakarta.annotation.PostConstruct;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Configuration
+public class MetricsConfig {
+
+ private final MeterRegistry meterRegistry;
+ private final AtomicInteger activeUsers = new AtomicInteger(0);
+
+ public MetricsConfig(MeterRegistry meterRegistry) {
+ this.meterRegistry = meterRegistry;
+ }
+
+ @PostConstruct
+ public void init() {
+ // Register a gauge for active users
+ Gauge.builder("application.active.users", activeUsers::get)
+ .description("Number of active users")
+ .register(meterRegistry);
+ }
+
+ // Method to update active users (could be called from your service layer)
+ public void updateActiveUsers(int count) {
+ activeUsers.set(count);
+ }
+}
\ No newline at end of file
diff --git a/microservices/course-composite-service/src/main/java/io/javatab/microservices/composite/course/config/SecurityConfig.java b/microservices/course-composite-service/src/main/java/io/javatab/microservices/composite/course/config/SecurityConfig.java
index e1760c3..68d9c8c 100644
--- a/microservices/course-composite-service/src/main/java/io/javatab/microservices/composite/course/config/SecurityConfig.java
+++ b/microservices/course-composite-service/src/main/java/io/javatab/microservices/composite/course/config/SecurityConfig.java
@@ -41,6 +41,7 @@ public SecurityConfig(@Value("${app.jwk-set-uri}") String jwkSetUri) {
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
+ .pathMatchers("/actuator/**", "/api/metrics/**").permitAll()
.pathMatchers("/api/course-aggregate/**").hasAnyRole("COURSE-READ", "REVIEW-READ")
.anyExchange().authenticated()
)
diff --git a/microservices/course-composite-service/src/main/resources/application.yml b/microservices/course-composite-service/src/main/resources/application.yml
index 480365f..840f2f8 100644
--- a/microservices/course-composite-service/src/main/resources/application.yml
+++ b/microservices/course-composite-service/src/main/resources/application.yml
@@ -23,6 +23,24 @@ logging:
issuer-uri: ${KEYCLOAK_ISSUER_URI:http://localhost:8081/realms/course-management-realm}
jwk-set-uri: ${KEYCLOAK_JWK_SET_URI:http://localhost:8081/realms/course-management-realm/protocol/openid-connect/certs}
+management:
+ endpoints:
+ web:
+ exposure:
+ include: "health,info,metrics,prometheus"
+ metrics:
+ export:
+ prometheus:
+ enabled: true
+ tags:
+ application: ${spring.application.name}
+ endpoint:
+ metrics:
+ enabled: true
+ prometheus:
+ enabled: true
+ health:
+ show-details: always
---
spring:
diff --git a/microservices/course-service/pom.xml b/microservices/course-service/pom.xml
index 2ec6903..ee87cdc 100644
--- a/microservices/course-service/pom.xml
+++ b/microservices/course-service/pom.xml
@@ -82,7 +82,10 @@
org.springframework.boot
spring-boot-starter-actuator
-
+
+ io.micrometer
+ micrometer-registry-prometheus
+
org.springframework.boot
spring-boot-starter-test
diff --git a/microservices/course-service/src/main/java/io/javatab/microservices/core/course/config/SecurityConfig.java b/microservices/course-service/src/main/java/io/javatab/microservices/core/course/config/SecurityConfig.java
index 66f2a28..7482d54 100644
--- a/microservices/course-service/src/main/java/io/javatab/microservices/core/course/config/SecurityConfig.java
+++ b/microservices/course-service/src/main/java/io/javatab/microservices/core/course/config/SecurityConfig.java
@@ -40,6 +40,7 @@ public SecurityConfig(@Value("${app.jwk-set-uri}") String jwkSetUri) {
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
+ .pathMatchers("/actuator/**").permitAll()
.pathMatchers("/api/courses/**").hasAnyRole("COURSE-READ", "COURSE-WRITE")
.anyExchange().authenticated()
)
diff --git a/microservices/course-service/src/main/resources/application.yml b/microservices/course-service/src/main/resources/application.yml
index 334a264..623c416 100644
--- a/microservices/course-service/src/main/resources/application.yml
+++ b/microservices/course-service/src/main/resources/application.yml
@@ -37,10 +37,20 @@ management:
endpoints:
web:
exposure:
- include: "*"
- endpoint:
- health:
- show-details: always
+ include: "health,info,metrics,prometheus"
+ metrics:
+ export:
+ prometheus:
+ enabled: true
+ tags:
+ application: ${spring.application.name}
+ endpoint:
+ metrics:
+ enabled: true
+ prometheus:
+ enabled: true
+ health:
+ show-details: always
---
app:
diff --git a/microservices/review-service/pom.xml b/microservices/review-service/pom.xml
index bd7147c..dc7da7e 100644
--- a/microservices/review-service/pom.xml
+++ b/microservices/review-service/pom.xml
@@ -74,7 +74,10 @@
org.springframework.boot
spring-boot-starter-actuator
-
+
+ io.micrometer
+ micrometer-registry-prometheus
+
org.springframework.boot
spring-boot-starter-test
diff --git a/microservices/review-service/src/main/java/io/javatab/microservices/core/review/config/SecurityConfig.java b/microservices/review-service/src/main/java/io/javatab/microservices/core/review/config/SecurityConfig.java
index 8ac3ffd..fb7d0ab 100644
--- a/microservices/review-service/src/main/java/io/javatab/microservices/core/review/config/SecurityConfig.java
+++ b/microservices/review-service/src/main/java/io/javatab/microservices/core/review/config/SecurityConfig.java
@@ -40,6 +40,7 @@ public SecurityConfig(@Value("${app.jwk-set-uri}") String jwkSetUri) {
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
+ .pathMatchers("/actuator/**").permitAll()
.pathMatchers("/api/reviews/**").hasAnyRole("REVIEW-READ", "REVIEW-WRITE")
.anyExchange().authenticated()
)
diff --git a/microservices/review-service/src/main/resources/application.yml b/microservices/review-service/src/main/resources/application.yml
index 02d43ad..3eecd03 100644
--- a/microservices/review-service/src/main/resources/application.yml
+++ b/microservices/review-service/src/main/resources/application.yml
@@ -30,10 +30,20 @@ management:
endpoints:
web:
exposure:
- include: "*"
- endpoint:
- health:
- show-details: always
+ include: "health,info,metrics,prometheus"
+ metrics:
+ export:
+ prometheus:
+ enabled: true
+ tags:
+ application: ${spring.application.name}
+ endpoint:
+ metrics:
+ enabled: true
+ prometheus:
+ enabled: true
+ health:
+ show-details: always
---
app:
diff --git a/spring-cloud/gateway-service/src/main/java/com/example/springcloud/gateway/config/SecurityConfig.java b/spring-cloud/gateway-service/src/main/java/com/example/springcloud/gateway/config/SecurityConfig.java
index 3cdb2c6..40f4bfb 100644
--- a/spring-cloud/gateway-service/src/main/java/com/example/springcloud/gateway/config/SecurityConfig.java
+++ b/spring-cloud/gateway-service/src/main/java/com/example/springcloud/gateway/config/SecurityConfig.java
@@ -35,7 +35,7 @@ public SecurityConfig(@Value("${app.jwk-set-uri}") String jwkSetUri) {
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
- .pathMatchers("/api/public").permitAll()
+ .pathMatchers("/api/public", "/actuator/**").permitAll()
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2