From 1b0ef74954f33727f9a33563904dda3c10a554ee Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:48:56 +0000 Subject: [PATCH] feat(EM-41): add ftgo-observability library with Micrometer/Prometheus metrics and Grafana dashboards - Create libs/ftgo-observability/ shared library module - Configure Micrometer Prometheus registry auto-configuration - Add custom business metrics for order, consumer, courier, restaurant domains - Create Prometheus scrape configuration for each service - Create Grafana dashboard JSON templates for each service - Add health and metrics actuator endpoint configuration - Update gradle/libs.versions.toml with observability dependencies - Add unit tests for all metrics classes and auto-configuration Co-Authored-By: Alex Baker --- gradle/libs.versions.toml | 5 + infrastructure/monitoring/README.md | 90 +++++++ .../consumer-service-dashboard.json | 222 ++++++++++++++++ .../dashboards/courier-service-dashboard.json | 237 +++++++++++++++++ .../dashboards/order-service-dashboard.json | 241 ++++++++++++++++++ .../restaurant-service-dashboard.json | 237 +++++++++++++++++ .../grafana/provisioning/dashboards.yml | 12 + .../grafana/provisioning/datasources.yml | 9 + .../monitoring/prometheus/prometheus.yml | 41 +++ libs/ftgo-observability/README.md | 125 +++++++++ libs/ftgo-observability/build.gradle | 38 +++ libs/ftgo-observability/settings.gradle | 1 + .../config/ActuatorEndpointConfiguration.java | 67 +++++ .../BusinessMetricsAutoConfiguration.java | 46 ++++ .../config/ObservabilityProperties.java | 57 +++++ .../PrometheusMetricsAutoConfiguration.java | 76 ++++++ .../metrics/ConsumerMetrics.java | 60 +++++ .../observability/metrics/CourierMetrics.java | 93 +++++++ .../observability/metrics/OrderMetrics.java | 89 +++++++ .../metrics/RestaurantMetrics.java | 81 ++++++ .../ftgo-observability-defaults.properties | 9 + ...ot.autoconfigure.AutoConfiguration.imports | 3 + ...rometheusMetricsAutoConfigurationTest.java | 57 +++++ .../metrics/ConsumerMetricsTest.java | 43 ++++ .../metrics/CourierMetricsTest.java | 55 ++++ .../metrics/OrderMetricsTest.java | 62 +++++ .../metrics/RestaurantMetricsTest.java | 55 ++++ 27 files changed, 2111 insertions(+) create mode 100644 infrastructure/monitoring/README.md create mode 100644 infrastructure/monitoring/grafana/dashboards/consumer-service-dashboard.json create mode 100644 infrastructure/monitoring/grafana/dashboards/courier-service-dashboard.json create mode 100644 infrastructure/monitoring/grafana/dashboards/order-service-dashboard.json create mode 100644 infrastructure/monitoring/grafana/dashboards/restaurant-service-dashboard.json create mode 100644 infrastructure/monitoring/grafana/provisioning/dashboards.yml create mode 100644 infrastructure/monitoring/grafana/provisioning/datasources.yml create mode 100644 infrastructure/monitoring/prometheus/prometheus.yml create mode 100644 libs/ftgo-observability/README.md create mode 100644 libs/ftgo-observability/build.gradle create mode 100644 libs/ftgo-observability/settings.gradle create mode 100644 libs/ftgo-observability/src/main/java/com/ftgo/observability/config/ActuatorEndpointConfiguration.java create mode 100644 libs/ftgo-observability/src/main/java/com/ftgo/observability/config/BusinessMetricsAutoConfiguration.java create mode 100644 libs/ftgo-observability/src/main/java/com/ftgo/observability/config/ObservabilityProperties.java create mode 100644 libs/ftgo-observability/src/main/java/com/ftgo/observability/config/PrometheusMetricsAutoConfiguration.java create mode 100644 libs/ftgo-observability/src/main/java/com/ftgo/observability/metrics/ConsumerMetrics.java create mode 100644 libs/ftgo-observability/src/main/java/com/ftgo/observability/metrics/CourierMetrics.java create mode 100644 libs/ftgo-observability/src/main/java/com/ftgo/observability/metrics/OrderMetrics.java create mode 100644 libs/ftgo-observability/src/main/java/com/ftgo/observability/metrics/RestaurantMetrics.java create mode 100644 libs/ftgo-observability/src/main/resources/META-INF/ftgo-observability-defaults.properties create mode 100644 libs/ftgo-observability/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 libs/ftgo-observability/src/test/java/com/ftgo/observability/config/PrometheusMetricsAutoConfigurationTest.java create mode 100644 libs/ftgo-observability/src/test/java/com/ftgo/observability/metrics/ConsumerMetricsTest.java create mode 100644 libs/ftgo-observability/src/test/java/com/ftgo/observability/metrics/CourierMetricsTest.java create mode 100644 libs/ftgo-observability/src/test/java/com/ftgo/observability/metrics/OrderMetricsTest.java create mode 100644 libs/ftgo-observability/src/test/java/com/ftgo/observability/metrics/RestaurantMetricsTest.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bee93101..785b52ea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,8 @@ java = "17" spring-boot = "3.2.5" spring-dependency-management = "1.1.4" micrometer = "1.12.5" +micrometer-tracing = "1.2.5" +prometheus-simpleclient = "0.16.0" junit-jupiter = "5.10.2" rest-assured = "5.4.0" flyway = "10.11.0" @@ -33,6 +35,8 @@ spring-boot-configuration-processor = { module = "org.springframework.boot:sprin micrometer-core = { module = "io.micrometer:micrometer-core", version.ref = "micrometer" } micrometer-registry-prometheus = { module = "io.micrometer:micrometer-registry-prometheus", version.ref = "micrometer" } +micrometer-observation = { module = "io.micrometer:micrometer-observation", version.ref = "micrometer" } +micrometer-tracing-bridge-otel = { module = "io.micrometer:micrometer-tracing-bridge-otel", version.ref = "micrometer-tracing" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit-jupiter" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit-jupiter" } @@ -74,6 +78,7 @@ jackson = ["jackson-databind", "jackson-datatype-jsr310"] testing = ["spring-boot-starter-test", "junit-jupiter-api", "junit-jupiter-engine", "mockito-junit-jupiter", "assertj-core"] rest-assured-all = ["rest-assured", "rest-assured-json-path", "rest-assured-spring-mock-mvc"] micrometer = ["micrometer-core", "micrometer-registry-prometheus"] +observability = ["micrometer-core", "micrometer-registry-prometheus", "micrometer-observation", "spring-boot-starter-actuator"] [plugins] spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" } diff --git a/infrastructure/monitoring/README.md b/infrastructure/monitoring/README.md new file mode 100644 index 00000000..555fbe78 --- /dev/null +++ b/infrastructure/monitoring/README.md @@ -0,0 +1,90 @@ +# FTGO Monitoring Infrastructure + +Prometheus and Grafana monitoring configuration for FTGO microservices. + +## Directory Structure + +``` +infrastructure/monitoring/ + prometheus/ + prometheus.yml # Prometheus scrape configuration + grafana/ + provisioning/ + datasources.yml # Grafana datasource provisioning + dashboards.yml # Grafana dashboard provisioning + dashboards/ + order-service-dashboard.json + consumer-service-dashboard.json + courier-service-dashboard.json + restaurant-service-dashboard.json +``` + +## Prometheus Configuration + +The `prometheus.yml` file configures scrape targets for all FTGO services: + +| Service | Target | Scrape Path | Interval | +|---------|--------|-------------|----------| +| order-service | order-service:8080 | /actuator/prometheus | 10s | +| consumer-service | consumer-service:8080 | /actuator/prometheus | 10s | +| courier-service | courier-service:8080 | /actuator/prometheus | 10s | +| restaurant-service | restaurant-service:8080 | /actuator/prometheus | 10s | + +### Customizing Targets + +For non-Docker environments, update the `targets` field with actual host:port values: + +```yaml +static_configs: + - targets: ['localhost:8081'] +``` + +## Grafana Dashboards + +### Dashboard Overview + +Each service dashboard contains three sections: + +1. **Business Metrics**: Domain-specific counters, rates, and gauges +2. **HTTP Metrics**: Request rates and p95 response times by endpoint +3. **JVM Metrics**: Heap memory usage and thread counts + +### Importing Dashboards + +Dashboards are automatically provisioned when using the provisioning configuration. For manual import: + +1. Open Grafana UI +2. Navigate to Dashboards > Import +3. Upload the JSON file or paste its contents +4. Select the Prometheus datasource + +### Dashboard UIDs + +| Dashboard | UID | +|-----------|-----| +| Order Service | ftgo-order-service | +| Consumer Service | ftgo-consumer-service | +| Courier Service | ftgo-courier-service | +| Restaurant Service | ftgo-restaurant-service | + +## Quick Start with Docker Compose + +```yaml +services: + prometheus: + image: prom/prometheus:v2.50.1 + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" + + grafana: + image: grafana/grafana:10.3.3 + volumes: + - ./grafana/provisioning:/etc/grafana/provisioning + - ./grafana/dashboards:/var/lib/grafana/dashboards + ports: + - "3000:3000" + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin +``` diff --git a/infrastructure/monitoring/grafana/dashboards/consumer-service-dashboard.json b/infrastructure/monitoring/grafana/dashboards/consumer-service-dashboard.json new file mode 100644 index 00000000..c43db834 --- /dev/null +++ b/infrastructure/monitoring/grafana/dashboards/consumer-service-dashboard.json @@ -0,0 +1,222 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, + "id": 1, + "title": "Business Metrics", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } + } + }, + "gridPos": { "h": 6, "w": 8, "x": 0, "y": 1 }, + "id": 2, + "options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }, + "title": "Consumers Registered (Total)", + "type": "stat", + "targets": [ + { + "expr": "ftgo_consumers_registered_total{application=\"consumer-service\"}", + "legendFormat": "Registered" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "thresholds": { "mode": "absolute", "steps": [{ "color": "blue", "value": null }] } + } + }, + "gridPos": { "h": 6, "w": 8, "x": 8, "y": 1 }, + "id": 3, + "options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }, + "title": "Active Consumers", + "type": "stat", + "targets": [ + { + "expr": "ftgo_consumers_active{application=\"consumer-service\"}", + "legendFormat": "Active" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "thresholds": { "mode": "absolute", "steps": [{ "color": "red", "value": null }] } + } + }, + "gridPos": { "h": 6, "w": 8, "x": 16, "y": 1 }, + "id": 4, + "options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }, + "title": "Verification Failures (Total)", + "type": "stat", + "targets": [ + { + "expr": "ftgo_consumers_verification_failures_total{application=\"consumer-service\"}", + "legendFormat": "Failures" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "unit": "reqps" + } + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 7 }, + "id": 5, + "title": "Consumer Registration Rate", + "type": "timeseries", + "targets": [ + { + "expr": "rate(ftgo_consumers_registered_total{application=\"consumer-service\"}[5m])", + "legendFormat": "Registrations/sec" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "unit": "reqps" + } + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 7 }, + "id": 6, + "title": "Verification Rate", + "type": "timeseries", + "targets": [ + { + "expr": "rate(ftgo_consumers_verifications_total{application=\"consumer-service\"}[5m])", + "legendFormat": "Verifications/sec" + }, + { + "expr": "rate(ftgo_consumers_verification_failures_total{application=\"consumer-service\"}[5m])", + "legendFormat": "Failures/sec" + } + ] + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 15 }, + "id": 7, + "title": "HTTP Metrics", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "unit": "reqps" + } + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 }, + "id": 8, + "title": "HTTP Request Rate", + "type": "timeseries", + "targets": [ + { + "expr": "sum(rate(http_server_requests_seconds_count{application=\"consumer-service\"}[5m])) by (uri, method)", + "legendFormat": "{{method}} {{uri}}" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "unit": "s" + } + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 }, + "id": 9, + "title": "HTTP Response Time (p95)", + "type": "timeseries", + "targets": [ + { + "expr": "histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{application=\"consumer-service\"}[5m])) by (le, uri))", + "legendFormat": "{{uri}}" + } + ] + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 24 }, + "id": 10, + "title": "JVM Metrics", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "unit": "bytes" + } + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 25 }, + "id": 11, + "title": "JVM Heap Memory", + "type": "timeseries", + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"consumer-service\", area=\"heap\"}", + "legendFormat": "Used {{id}}" + }, + { + "expr": "jvm_memory_max_bytes{application=\"consumer-service\", area=\"heap\"}", + "legendFormat": "Max {{id}}" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 25 }, + "id": 12, + "title": "JVM Threads", + "type": "timeseries", + "targets": [ + { + "expr": "jvm_threads_live_threads{application=\"consumer-service\"}", + "legendFormat": "Live Threads" + }, + { + "expr": "jvm_threads_daemon_threads{application=\"consumer-service\"}", + "legendFormat": "Daemon Threads" + } + ] + } + ], + "schemaVersion": 39, + "tags": ["ftgo", "consumer-service"], + "templating": { + "list": [ + { + "current": { "selected": false, "text": "Prometheus", "value": "Prometheus" }, + "hide": 0, + "includeAll": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "type": "datasource" + } + ] + }, + "time": { "from": "now-1h", "to": "now" }, + "title": "FTGO Consumer Service", + "uid": "ftgo-consumer-service" +} diff --git a/infrastructure/monitoring/grafana/dashboards/courier-service-dashboard.json b/infrastructure/monitoring/grafana/dashboards/courier-service-dashboard.json new file mode 100644 index 00000000..0504bd68 --- /dev/null +++ b/infrastructure/monitoring/grafana/dashboards/courier-service-dashboard.json @@ -0,0 +1,237 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, + "id": 1, + "title": "Business Metrics", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } + } + }, + "gridPos": { "h": 6, "w": 6, "x": 0, "y": 1 }, + "id": 2, + "options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }, + "title": "Deliveries Completed (Total)", + "type": "stat", + "targets": [ + { + "expr": "ftgo_deliveries_completed_total{application=\"courier-service\"}", + "legendFormat": "Completed" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "thresholds": { "mode": "absolute", "steps": [{ "color": "red", "value": null }] } + } + }, + "gridPos": { "h": 6, "w": 6, "x": 6, "y": 1 }, + "id": 3, + "options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }, + "title": "Deliveries Failed (Total)", + "type": "stat", + "targets": [ + { + "expr": "ftgo_deliveries_failed_total{application=\"courier-service\"}", + "legendFormat": "Failed" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "thresholds": { "mode": "absolute", "steps": [{ "color": "blue", "value": null }] } + } + }, + "gridPos": { "h": 6, "w": 6, "x": 12, "y": 1 }, + "id": 4, + "options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }, + "title": "Available Couriers", + "type": "stat", + "targets": [ + { + "expr": "ftgo_couriers_available{application=\"courier-service\"}", + "legendFormat": "Available" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } + } + }, + "gridPos": { "h": 6, "w": 6, "x": 18, "y": 1 }, + "id": 5, + "options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }, + "title": "Couriers Created (Total)", + "type": "stat", + "targets": [ + { + "expr": "ftgo_couriers_created_total{application=\"courier-service\"}", + "legendFormat": "Created" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "unit": "s" + } + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 7 }, + "id": 6, + "title": "Delivery Duration", + "type": "timeseries", + "targets": [ + { + "expr": "rate(ftgo_deliveries_duration_seconds_sum{application=\"courier-service\"}[5m]) / rate(ftgo_deliveries_duration_seconds_count{application=\"courier-service\"}[5m])", + "legendFormat": "Avg Delivery Time" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "unit": "km" + } + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 7 }, + "id": 7, + "title": "Delivery Distance Distribution", + "type": "timeseries", + "targets": [ + { + "expr": "ftgo_deliveries_distance_km_sum{application=\"courier-service\"} / ftgo_deliveries_distance_km_count{application=\"courier-service\"}", + "legendFormat": "Avg Distance" + } + ] + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 15 }, + "id": 8, + "title": "HTTP Metrics", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "unit": "reqps" + } + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 }, + "id": 9, + "title": "HTTP Request Rate", + "type": "timeseries", + "targets": [ + { + "expr": "sum(rate(http_server_requests_seconds_count{application=\"courier-service\"}[5m])) by (uri, method)", + "legendFormat": "{{method}} {{uri}}" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "unit": "s" + } + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 }, + "id": 10, + "title": "HTTP Response Time (p95)", + "type": "timeseries", + "targets": [ + { + "expr": "histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{application=\"courier-service\"}[5m])) by (le, uri))", + "legendFormat": "{{uri}}" + } + ] + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 24 }, + "id": 11, + "title": "JVM Metrics", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "unit": "bytes" + } + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 25 }, + "id": 12, + "title": "JVM Heap Memory", + "type": "timeseries", + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"courier-service\", area=\"heap\"}", + "legendFormat": "Used {{id}}" + }, + { + "expr": "jvm_memory_max_bytes{application=\"courier-service\", area=\"heap\"}", + "legendFormat": "Max {{id}}" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 25 }, + "id": 13, + "title": "JVM Threads", + "type": "timeseries", + "targets": [ + { + "expr": "jvm_threads_live_threads{application=\"courier-service\"}", + "legendFormat": "Live Threads" + }, + { + "expr": "jvm_threads_daemon_threads{application=\"courier-service\"}", + "legendFormat": "Daemon Threads" + } + ] + } + ], + "schemaVersion": 39, + "tags": ["ftgo", "courier-service"], + "templating": { + "list": [ + { + "current": { "selected": false, "text": "Prometheus", "value": "Prometheus" }, + "hide": 0, + "includeAll": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "type": "datasource" + } + ] + }, + "time": { "from": "now-1h", "to": "now" }, + "title": "FTGO Courier Service", + "uid": "ftgo-courier-service" +} diff --git a/infrastructure/monitoring/grafana/dashboards/order-service-dashboard.json b/infrastructure/monitoring/grafana/dashboards/order-service-dashboard.json new file mode 100644 index 00000000..ee2e439b --- /dev/null +++ b/infrastructure/monitoring/grafana/dashboards/order-service-dashboard.json @@ -0,0 +1,241 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, + "id": 1, + "title": "Business Metrics", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } + } + }, + "gridPos": { "h": 6, "w": 6, "x": 0, "y": 1 }, + "id": 2, + "options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }, + "title": "Orders Created (Total)", + "type": "stat", + "targets": [ + { + "expr": "ftgo_orders_created_total{application=\"order-service\"}", + "legendFormat": "Total Orders" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } + } + }, + "gridPos": { "h": 6, "w": 6, "x": 6, "y": 1 }, + "id": 3, + "options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }, + "title": "Orders Approved (Total)", + "type": "stat", + "targets": [ + { + "expr": "ftgo_orders_approved_total{application=\"order-service\"}", + "legendFormat": "Approved" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "thresholds": { "mode": "absolute", "steps": [{ "color": "red", "value": null }] } + } + }, + "gridPos": { "h": 6, "w": 6, "x": 12, "y": 1 }, + "id": 4, + "options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }, + "title": "Orders Rejected (Total)", + "type": "stat", + "targets": [ + { + "expr": "ftgo_orders_rejected_total{application=\"order-service\"}", + "legendFormat": "Rejected" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "thresholds": { "mode": "absolute", "steps": [{ "color": "orange", "value": null }] } + } + }, + "gridPos": { "h": 6, "w": 6, "x": 18, "y": 1 }, + "id": 5, + "options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }, + "title": "Orders Cancelled (Total)", + "type": "stat", + "targets": [ + { + "expr": "ftgo_orders_cancelled_total{application=\"order-service\"}", + "legendFormat": "Cancelled" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "unit": "reqps" + } + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 7 }, + "id": 6, + "title": "Order Creation Rate", + "type": "timeseries", + "targets": [ + { + "expr": "rate(ftgo_orders_created_total{application=\"order-service\"}[5m])", + "legendFormat": "Orders/sec" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "unit": "s" + } + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 7 }, + "id": 7, + "title": "Order Fulfillment Duration", + "type": "timeseries", + "targets": [ + { + "expr": "rate(ftgo_orders_fulfillment_duration_seconds_sum{application=\"order-service\"}[5m]) / rate(ftgo_orders_fulfillment_duration_seconds_count{application=\"order-service\"}[5m])", + "legendFormat": "Avg Fulfillment Time" + } + ] + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 15 }, + "id": 8, + "title": "HTTP Metrics", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "unit": "reqps" + } + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 }, + "id": 9, + "title": "HTTP Request Rate", + "type": "timeseries", + "targets": [ + { + "expr": "sum(rate(http_server_requests_seconds_count{application=\"order-service\"}[5m])) by (uri, method)", + "legendFormat": "{{method}} {{uri}}" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "unit": "s" + } + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 }, + "id": 10, + "title": "HTTP Response Time (p95)", + "type": "timeseries", + "targets": [ + { + "expr": "histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{application=\"order-service\"}[5m])) by (le, uri))", + "legendFormat": "{{uri}}" + } + ] + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 24 }, + "id": 11, + "title": "JVM Metrics", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "unit": "bytes" + } + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 25 }, + "id": 12, + "title": "JVM Heap Memory", + "type": "timeseries", + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"order-service\", area=\"heap\"}", + "legendFormat": "Used {{id}}" + }, + { + "expr": "jvm_memory_max_bytes{application=\"order-service\", area=\"heap\"}", + "legendFormat": "Max {{id}}" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 25 }, + "id": 13, + "title": "JVM Threads", + "type": "timeseries", + "targets": [ + { + "expr": "jvm_threads_live_threads{application=\"order-service\"}", + "legendFormat": "Live Threads" + }, + { + "expr": "jvm_threads_daemon_threads{application=\"order-service\"}", + "legendFormat": "Daemon Threads" + } + ] + } + ], + "schemaVersion": 39, + "tags": ["ftgo", "order-service"], + "templating": { + "list": [ + { + "current": { "selected": false, "text": "Prometheus", "value": "Prometheus" }, + "hide": 0, + "includeAll": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "type": "datasource" + } + ] + }, + "time": { "from": "now-1h", "to": "now" }, + "title": "FTGO Order Service", + "uid": "ftgo-order-service" +} diff --git a/infrastructure/monitoring/grafana/dashboards/restaurant-service-dashboard.json b/infrastructure/monitoring/grafana/dashboards/restaurant-service-dashboard.json new file mode 100644 index 00000000..e3dab656 --- /dev/null +++ b/infrastructure/monitoring/grafana/dashboards/restaurant-service-dashboard.json @@ -0,0 +1,237 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, + "id": 1, + "title": "Business Metrics", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } + } + }, + "gridPos": { "h": 6, "w": 6, "x": 0, "y": 1 }, + "id": 2, + "options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }, + "title": "Restaurants Created (Total)", + "type": "stat", + "targets": [ + { + "expr": "ftgo_restaurants_created_total{application=\"restaurant-service\"}", + "legendFormat": "Created" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "thresholds": { "mode": "absolute", "steps": [{ "color": "blue", "value": null }] } + } + }, + "gridPos": { "h": 6, "w": 6, "x": 6, "y": 1 }, + "id": 3, + "options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }, + "title": "Active Restaurants", + "type": "stat", + "targets": [ + { + "expr": "ftgo_restaurants_active{application=\"restaurant-service\"}", + "legendFormat": "Active" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } + } + }, + "gridPos": { "h": 6, "w": 6, "x": 12, "y": 1 }, + "id": 4, + "options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }, + "title": "Tickets Accepted (Total)", + "type": "stat", + "targets": [ + { + "expr": "ftgo_tickets_accepted_total{application=\"restaurant-service\"}", + "legendFormat": "Accepted" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "thresholds": { "mode": "absolute", "steps": [{ "color": "red", "value": null }] } + } + }, + "gridPos": { "h": 6, "w": 6, "x": 18, "y": 1 }, + "id": 5, + "options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }, + "title": "Tickets Rejected (Total)", + "type": "stat", + "targets": [ + { + "expr": "ftgo_tickets_rejected_total{application=\"restaurant-service\"}", + "legendFormat": "Rejected" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "unit": "reqps" + } + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 7 }, + "id": 6, + "title": "Menu Revision Rate", + "type": "timeseries", + "targets": [ + { + "expr": "rate(ftgo_restaurants_menu_revisions_total{application=\"restaurant-service\"}[5m])", + "legendFormat": "Revisions/sec" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "unit": "s" + } + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 7 }, + "id": 7, + "title": "Ticket Preparation Duration", + "type": "timeseries", + "targets": [ + { + "expr": "rate(ftgo_tickets_preparation_duration_seconds_sum{application=\"restaurant-service\"}[5m]) / rate(ftgo_tickets_preparation_duration_seconds_count{application=\"restaurant-service\"}[5m])", + "legendFormat": "Avg Preparation Time" + } + ] + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 15 }, + "id": 8, + "title": "HTTP Metrics", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "unit": "reqps" + } + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 }, + "id": 9, + "title": "HTTP Request Rate", + "type": "timeseries", + "targets": [ + { + "expr": "sum(rate(http_server_requests_seconds_count{application=\"restaurant-service\"}[5m])) by (uri, method)", + "legendFormat": "{{method}} {{uri}}" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "unit": "s" + } + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 }, + "id": 10, + "title": "HTTP Response Time (p95)", + "type": "timeseries", + "targets": [ + { + "expr": "histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{application=\"restaurant-service\"}[5m])) by (le, uri))", + "legendFormat": "{{uri}}" + } + ] + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 24 }, + "id": 11, + "title": "JVM Metrics", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "unit": "bytes" + } + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 25 }, + "id": 12, + "title": "JVM Heap Memory", + "type": "timeseries", + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"restaurant-service\", area=\"heap\"}", + "legendFormat": "Used {{id}}" + }, + { + "expr": "jvm_memory_max_bytes{application=\"restaurant-service\", area=\"heap\"}", + "legendFormat": "Max {{id}}" + } + ] + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 25 }, + "id": 13, + "title": "JVM Threads", + "type": "timeseries", + "targets": [ + { + "expr": "jvm_threads_live_threads{application=\"restaurant-service\"}", + "legendFormat": "Live Threads" + }, + { + "expr": "jvm_threads_daemon_threads{application=\"restaurant-service\"}", + "legendFormat": "Daemon Threads" + } + ] + } + ], + "schemaVersion": 39, + "tags": ["ftgo", "restaurant-service"], + "templating": { + "list": [ + { + "current": { "selected": false, "text": "Prometheus", "value": "Prometheus" }, + "hide": 0, + "includeAll": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "type": "datasource" + } + ] + }, + "time": { "from": "now-1h", "to": "now" }, + "title": "FTGO Restaurant Service", + "uid": "ftgo-restaurant-service" +} diff --git a/infrastructure/monitoring/grafana/provisioning/dashboards.yml b/infrastructure/monitoring/grafana/provisioning/dashboards.yml new file mode 100644 index 00000000..0f828ee0 --- /dev/null +++ b/infrastructure/monitoring/grafana/provisioning/dashboards.yml @@ -0,0 +1,12 @@ +apiVersion: 1 + +providers: + - name: 'FTGO Dashboards' + orgId: 1 + folder: 'FTGO' + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards + foldersFromFilesStructure: false diff --git a/infrastructure/monitoring/grafana/provisioning/datasources.yml b/infrastructure/monitoring/grafana/provisioning/datasources.yml new file mode 100644 index 00000000..bb009bb2 --- /dev/null +++ b/infrastructure/monitoring/grafana/provisioning/datasources.yml @@ -0,0 +1,9 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: false diff --git a/infrastructure/monitoring/prometheus/prometheus.yml b/infrastructure/monitoring/prometheus/prometheus.yml new file mode 100644 index 00000000..ceb8434a --- /dev/null +++ b/infrastructure/monitoring/prometheus/prometheus.yml @@ -0,0 +1,41 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + scrape_timeout: 10s + +scrape_configs: + - job_name: 'order-service' + metrics_path: '/actuator/prometheus' + scrape_interval: 10s + static_configs: + - targets: ['order-service:8080'] + labels: + service: 'order-service' + domain: 'order' + + - job_name: 'consumer-service' + metrics_path: '/actuator/prometheus' + scrape_interval: 10s + static_configs: + - targets: ['consumer-service:8080'] + labels: + service: 'consumer-service' + domain: 'consumer' + + - job_name: 'courier-service' + metrics_path: '/actuator/prometheus' + scrape_interval: 10s + static_configs: + - targets: ['courier-service:8080'] + labels: + service: 'courier-service' + domain: 'courier' + + - job_name: 'restaurant-service' + metrics_path: '/actuator/prometheus' + scrape_interval: 10s + static_configs: + - targets: ['restaurant-service:8080'] + labels: + service: 'restaurant-service' + domain: 'restaurant' diff --git a/libs/ftgo-observability/README.md b/libs/ftgo-observability/README.md new file mode 100644 index 00000000..2b0483c1 --- /dev/null +++ b/libs/ftgo-observability/README.md @@ -0,0 +1,125 @@ +# ftgo-observability + +Shared observability library for FTGO microservices providing Micrometer/Prometheus metrics auto-configuration, custom business metrics, and actuator endpoint configuration. + +## Overview + +This module provides: + +- **Prometheus Metrics Auto-Configuration**: Automatically configures Micrometer with Prometheus registry, common tags, and JVM/system metrics +- **Custom Business Metrics**: Domain-specific metrics for each bounded context (orders, consumers, couriers, restaurants) +- **Actuator Endpoint Configuration**: Pre-configured actuator endpoints for health, info, metrics, and Prometheus scraping + +## Usage + +Add this library as a dependency in your service's `build.gradle`: + +```groovy +dependencies { + implementation project(':libs:ftgo-observability') +} +``` + +The auto-configuration will automatically register: +- Prometheus metrics endpoint at `/actuator/prometheus` +- Health endpoint at `/actuator/health` +- Info endpoint at `/actuator/info` +- Metrics endpoint at `/actuator/metrics` + +## Configuration Properties + +```yaml +ftgo: + observability: + application-name: my-service # Override spring.application.name for metrics tags + environment: production # Environment tag (defaults to 'development') + metrics: + enabled: true # Enable/disable all metrics (default: true) + business-metrics-enabled: true # Enable/disable business metrics (default: true) + order-metrics-enabled: true # Enable/disable order metrics (default: true) + consumer-metrics-enabled: true # Enable/disable consumer metrics (default: true) + courier-metrics-enabled: true # Enable/disable courier metrics (default: true) + restaurant-metrics-enabled: true # Enable/disable restaurant metrics (default: true) +``` + +## Metrics Naming Conventions + +All custom business metrics follow the naming pattern: `ftgo..` + +| Prefix | Domain | Description | +|---------------------|------------|--------------------------------| +| `ftgo.orders.*` | order | Order lifecycle metrics | +| `ftgo.consumers.*` | consumer | Consumer registration/verification | +| `ftgo.deliveries.*` | courier | Delivery tracking metrics | +| `ftgo.couriers.*` | courier | Courier availability metrics | +| `ftgo.restaurants.*` | restaurant | Restaurant management metrics | +| `ftgo.tickets.*` | restaurant | Kitchen ticket metrics | + +### Metric Types + +- **Counters**: Monotonically increasing values (e.g., `ftgo.orders.created`) +- **Gauges**: Current values that can go up/down (e.g., `ftgo.couriers.available`) +- **Timers**: Duration measurements (e.g., `ftgo.orders.fulfillment.duration`) +- **Distribution Summaries**: Value distributions (e.g., `ftgo.deliveries.distance`) + +### Common Tags + +All metrics are automatically tagged with: +- `application`: The service name (from `spring.application.name`) +- `env`: The deployment environment + +## Business Metrics Reference + +### Order Metrics (`OrderMetrics`) +| Metric | Type | Description | +|--------|------|-------------| +| `ftgo.orders.created` | Counter | Total orders created | +| `ftgo.orders.approved` | Counter | Total orders approved | +| `ftgo.orders.rejected` | Counter | Total orders rejected | +| `ftgo.orders.cancelled` | Counter | Total orders cancelled | +| `ftgo.orders.revised` | Counter | Total orders revised | +| `ftgo.orders.fulfillment.duration` | Timer | Order fulfillment time | +| `ftgo.orders.approval.duration` | Timer | Order approval time | + +### Consumer Metrics (`ConsumerMetrics`) +| Metric | Type | Description | +|--------|------|-------------| +| `ftgo.consumers.registered` | Counter | Total consumers registered | +| `ftgo.consumers.verifications` | Counter | Total verifications performed | +| `ftgo.consumers.verification.failures` | Counter | Total verification failures | +| `ftgo.consumers.active` | Gauge | Current active consumers | + +### Courier Metrics (`CourierMetrics`) +| Metric | Type | Description | +|--------|------|-------------| +| `ftgo.deliveries.completed` | Counter | Total deliveries completed | +| `ftgo.deliveries.failed` | Counter | Total deliveries failed | +| `ftgo.couriers.created` | Counter | Total couriers created | +| `ftgo.deliveries.duration` | Timer | Delivery duration (pickup to completion) | +| `ftgo.deliveries.pickup.duration` | Timer | Pickup duration (assignment to pickup) | +| `ftgo.deliveries.distance` | Summary | Delivery distance distribution (km) | +| `ftgo.couriers.available` | Gauge | Current available couriers | + +### Restaurant Metrics (`RestaurantMetrics`) +| Metric | Type | Description | +|--------|------|-------------| +| `ftgo.restaurants.created` | Counter | Total restaurants created | +| `ftgo.restaurants.menu.revisions` | Counter | Total menu revisions | +| `ftgo.tickets.accepted` | Counter | Total tickets accepted | +| `ftgo.tickets.rejected` | Counter | Total tickets rejected | +| `ftgo.tickets.preparation.duration` | Timer | Ticket preparation time | +| `ftgo.restaurants.active` | Gauge | Current active restaurants | + +## Grafana Dashboards + +Pre-built Grafana dashboard templates are available in `infrastructure/monitoring/grafana/dashboards/`: + +- `order-service-dashboard.json` - Order lifecycle, fulfillment times, HTTP metrics +- `consumer-service-dashboard.json` - Consumer registrations, verifications, HTTP metrics +- `courier-service-dashboard.json` - Delivery tracking, courier availability, HTTP metrics +- `restaurant-service-dashboard.json` - Restaurant management, ticket processing, HTTP metrics + +Each dashboard includes: +1. **Business Metrics** - Domain-specific counters and rates +2. **HTTP Metrics** - Request rates and response time percentiles +3. **JVM Metrics** - Heap memory usage and thread counts diff --git a/libs/ftgo-observability/build.gradle b/libs/ftgo-observability/build.gradle new file mode 100644 index 00000000..21af81d2 --- /dev/null +++ b/libs/ftgo-observability/build.gradle @@ -0,0 +1,38 @@ +plugins { + id 'java-library' +} + +group = 'com.ftgo' +version = '1.0.0-SNAPSHOT' +sourceCompatibility = '17' +targetCompatibility = '17' + +repositories { + mavenCentral() +} + +dependencies { + api 'org.springframework.boot:spring-boot-starter-actuator:3.2.5' + api 'io.micrometer:micrometer-core:1.12.5' + api 'io.micrometer:micrometer-registry-prometheus:1.12.5' + api 'io.micrometer:micrometer-observation:1.12.5' + + implementation 'org.springframework.boot:spring-boot-autoconfigure:3.2.5' + implementation 'org.springframework.boot:spring-boot-starter:3.2.5' + implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1' + + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor:3.2.5' + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testImplementation 'org.assertj:assertj-core:3.25.3' + testImplementation 'org.springframework.boot:spring-boot-starter-test:3.2.5' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +test { + useJUnitPlatform() +} + +java { + withSourcesJar() +} diff --git a/libs/ftgo-observability/settings.gradle b/libs/ftgo-observability/settings.gradle new file mode 100644 index 00000000..82ed9d47 --- /dev/null +++ b/libs/ftgo-observability/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'ftgo-observability' diff --git a/libs/ftgo-observability/src/main/java/com/ftgo/observability/config/ActuatorEndpointConfiguration.java b/libs/ftgo-observability/src/main/java/com/ftgo/observability/config/ActuatorEndpointConfiguration.java new file mode 100644 index 00000000..d98d94d2 --- /dev/null +++ b/libs/ftgo-observability/src/main/java/com/ftgo/observability/config/ActuatorEndpointConfiguration.java @@ -0,0 +1,67 @@ +package com.ftgo.observability.config; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@AutoConfiguration +@ConditionalOnClass(name = "org.springframework.boot.actuate.endpoint.annotation.Endpoint") +public class ActuatorEndpointConfiguration { + + @Bean + @ConditionalOnMissingBean(name = "actuatorEndpointProperties") + public ActuatorEndpointProperties actuatorEndpointProperties() { + return new ActuatorEndpointProperties(); + } + + public static class ActuatorEndpointProperties { + + private String basePath = "/actuator"; + private boolean prometheusEnabled = true; + private boolean healthEnabled = true; + private boolean infoEnabled = true; + private boolean metricsEnabled = true; + + public String getBasePath() { + return basePath; + } + + public void setBasePath(String basePath) { + this.basePath = basePath; + } + + public boolean isPrometheusEnabled() { + return prometheusEnabled; + } + + public void setPrometheusEnabled(boolean prometheusEnabled) { + this.prometheusEnabled = prometheusEnabled; + } + + public boolean isHealthEnabled() { + return healthEnabled; + } + + public void setHealthEnabled(boolean healthEnabled) { + this.healthEnabled = healthEnabled; + } + + public boolean isInfoEnabled() { + return infoEnabled; + } + + public void setInfoEnabled(boolean infoEnabled) { + this.infoEnabled = infoEnabled; + } + + public boolean isMetricsEnabled() { + return metricsEnabled; + } + + public void setMetricsEnabled(boolean metricsEnabled) { + this.metricsEnabled = metricsEnabled; + } + } +} diff --git a/libs/ftgo-observability/src/main/java/com/ftgo/observability/config/BusinessMetricsAutoConfiguration.java b/libs/ftgo-observability/src/main/java/com/ftgo/observability/config/BusinessMetricsAutoConfiguration.java new file mode 100644 index 00000000..a5cc02b4 --- /dev/null +++ b/libs/ftgo-observability/src/main/java/com/ftgo/observability/config/BusinessMetricsAutoConfiguration.java @@ -0,0 +1,46 @@ +package com.ftgo.observability.config; + +import com.ftgo.observability.metrics.ConsumerMetrics; +import com.ftgo.observability.metrics.CourierMetrics; +import com.ftgo.observability.metrics.OrderMetrics; +import com.ftgo.observability.metrics.RestaurantMetrics; +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration(after = PrometheusMetricsAutoConfiguration.class) +@ConditionalOnBean(MeterRegistry.class) +@ConditionalOnProperty(prefix = "ftgo.observability.metrics", name = "business-metrics-enabled", havingValue = "true", matchIfMissing = true) +public class BusinessMetricsAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "ftgo.observability.metrics", name = "order-metrics-enabled", havingValue = "true", matchIfMissing = true) + public OrderMetrics orderMetrics(MeterRegistry registry) { + return new OrderMetrics(registry); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "ftgo.observability.metrics", name = "consumer-metrics-enabled", havingValue = "true", matchIfMissing = true) + public ConsumerMetrics consumerMetrics(MeterRegistry registry) { + return new ConsumerMetrics(registry); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "ftgo.observability.metrics", name = "courier-metrics-enabled", havingValue = "true", matchIfMissing = true) + public CourierMetrics courierMetrics(MeterRegistry registry) { + return new CourierMetrics(registry); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "ftgo.observability.metrics", name = "restaurant-metrics-enabled", havingValue = "true", matchIfMissing = true) + public RestaurantMetrics restaurantMetrics(MeterRegistry registry) { + return new RestaurantMetrics(registry); + } +} diff --git a/libs/ftgo-observability/src/main/java/com/ftgo/observability/config/ObservabilityProperties.java b/libs/ftgo-observability/src/main/java/com/ftgo/observability/config/ObservabilityProperties.java new file mode 100644 index 00000000..b4065fb5 --- /dev/null +++ b/libs/ftgo-observability/src/main/java/com/ftgo/observability/config/ObservabilityProperties.java @@ -0,0 +1,57 @@ +package com.ftgo.observability.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "ftgo.observability") +public class ObservabilityProperties { + + private String applicationName; + private String environment; + private Metrics metrics = new Metrics(); + + public String getApplicationName() { + return applicationName; + } + + public void setApplicationName(String applicationName) { + this.applicationName = applicationName; + } + + public String getEnvironment() { + return environment; + } + + public void setEnvironment(String environment) { + this.environment = environment; + } + + public Metrics getMetrics() { + return metrics; + } + + public void setMetrics(Metrics metrics) { + this.metrics = metrics; + } + + public static class Metrics { + + private boolean enabled = true; + private boolean businessMetricsEnabled = true; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isBusinessMetricsEnabled() { + return businessMetricsEnabled; + } + + public void setBusinessMetricsEnabled(boolean businessMetricsEnabled) { + this.businessMetricsEnabled = businessMetricsEnabled; + } + } +} diff --git a/libs/ftgo-observability/src/main/java/com/ftgo/observability/config/PrometheusMetricsAutoConfiguration.java b/libs/ftgo-observability/src/main/java/com/ftgo/observability/config/PrometheusMetricsAutoConfiguration.java new file mode 100644 index 00000000..cf5227b2 --- /dev/null +++ b/libs/ftgo-observability/src/main/java/com/ftgo/observability/config/PrometheusMetricsAutoConfiguration.java @@ -0,0 +1,76 @@ +package com.ftgo.observability.config; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics; +import io.micrometer.core.instrument.binder.system.ProcessorMetrics; +import io.micrometer.core.instrument.binder.system.UptimeMetrics; +import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; + +@AutoConfiguration +@ConditionalOnClass(MeterRegistry.class) +@EnableConfigurationProperties(ObservabilityProperties.class) +public class PrometheusMetricsAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public MeterRegistryCustomizer commonTagsCustomizer( + ObservabilityProperties properties, Environment environment) { + String applicationName = properties.getApplicationName() != null + ? properties.getApplicationName() + : environment.getProperty("spring.application.name", "unknown"); + String environmentName = properties.getEnvironment() != null + ? properties.getEnvironment() + : environment.getProperty("ftgo.environment", "development"); + + return registry -> registry.config() + .commonTags( + "application", applicationName, + "env", environmentName + ); + } + + @Bean + @ConditionalOnMissingBean(JvmMemoryMetrics.class) + public JvmMemoryMetrics jvmMemoryMetrics() { + return new JvmMemoryMetrics(); + } + + @Bean + @ConditionalOnMissingBean(JvmGcMetrics.class) + public JvmGcMetrics jvmGcMetrics() { + return new JvmGcMetrics(); + } + + @Bean + @ConditionalOnMissingBean(JvmThreadMetrics.class) + public JvmThreadMetrics jvmThreadMetrics() { + return new JvmThreadMetrics(); + } + + @Bean + @ConditionalOnMissingBean(ClassLoaderMetrics.class) + public ClassLoaderMetrics classLoaderMetrics() { + return new ClassLoaderMetrics(); + } + + @Bean + @ConditionalOnMissingBean(ProcessorMetrics.class) + public ProcessorMetrics processorMetrics() { + return new ProcessorMetrics(); + } + + @Bean + @ConditionalOnMissingBean(UptimeMetrics.class) + public UptimeMetrics uptimeMetrics() { + return new UptimeMetrics(); + } +} diff --git a/libs/ftgo-observability/src/main/java/com/ftgo/observability/metrics/ConsumerMetrics.java b/libs/ftgo-observability/src/main/java/com/ftgo/observability/metrics/ConsumerMetrics.java new file mode 100644 index 00000000..27cbca17 --- /dev/null +++ b/libs/ftgo-observability/src/main/java/com/ftgo/observability/metrics/ConsumerMetrics.java @@ -0,0 +1,60 @@ +package com.ftgo.observability.metrics; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import jakarta.annotation.PostConstruct; +import java.util.concurrent.atomic.AtomicLong; + +public class ConsumerMetrics { + + private final MeterRegistry registry; + + private Counter consumersRegisteredCounter; + private Counter consumerVerificationsCounter; + private Counter consumerVerificationFailuresCounter; + private final AtomicLong activeConsumers = new AtomicLong(0); + + public ConsumerMetrics(MeterRegistry registry) { + this.registry = registry; + } + + @PostConstruct + public void init() { + consumersRegisteredCounter = Counter.builder("ftgo.consumers.registered") + .description("Total number of consumers registered") + .tag("domain", "consumer") + .register(registry); + + consumerVerificationsCounter = Counter.builder("ftgo.consumers.verifications") + .description("Total number of consumer verifications performed") + .tag("domain", "consumer") + .register(registry); + + consumerVerificationFailuresCounter = Counter.builder("ftgo.consumers.verification.failures") + .description("Total number of consumer verification failures") + .tag("domain", "consumer") + .register(registry); + + Gauge.builder("ftgo.consumers.active", activeConsumers, AtomicLong::doubleValue) + .description("Current number of active consumers") + .tag("domain", "consumer") + .register(registry); + } + + public void incrementConsumersRegistered() { + consumersRegisteredCounter.increment(); + } + + public void incrementConsumerVerifications() { + consumerVerificationsCounter.increment(); + } + + public void incrementConsumerVerificationFailures() { + consumerVerificationFailuresCounter.increment(); + } + + public void setActiveConsumers(long count) { + activeConsumers.set(count); + } +} diff --git a/libs/ftgo-observability/src/main/java/com/ftgo/observability/metrics/CourierMetrics.java b/libs/ftgo-observability/src/main/java/com/ftgo/observability/metrics/CourierMetrics.java new file mode 100644 index 00000000..3c311041 --- /dev/null +++ b/libs/ftgo-observability/src/main/java/com/ftgo/observability/metrics/CourierMetrics.java @@ -0,0 +1,93 @@ +package com.ftgo.observability.metrics; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import jakarta.annotation.PostConstruct; +import java.util.concurrent.atomic.AtomicLong; + +public class CourierMetrics { + + private final MeterRegistry registry; + + private Counter deliveriesCompletedCounter; + private Counter deliveriesFailedCounter; + private Counter couriersCreatedCounter; + private Timer deliveryDurationTimer; + private Timer deliveryPickupTimer; + private DistributionSummary deliveryDistanceSummary; + private final AtomicLong availableCouriers = new AtomicLong(0); + + public CourierMetrics(MeterRegistry registry) { + this.registry = registry; + } + + @PostConstruct + public void init() { + deliveriesCompletedCounter = Counter.builder("ftgo.deliveries.completed") + .description("Total number of deliveries completed") + .tag("domain", "courier") + .register(registry); + + deliveriesFailedCounter = Counter.builder("ftgo.deliveries.failed") + .description("Total number of deliveries that failed") + .tag("domain", "courier") + .register(registry); + + couriersCreatedCounter = Counter.builder("ftgo.couriers.created") + .description("Total number of couriers created") + .tag("domain", "courier") + .register(registry); + + deliveryDurationTimer = Timer.builder("ftgo.deliveries.duration") + .description("Time taken from pickup to delivery completion") + .tag("domain", "courier") + .register(registry); + + deliveryPickupTimer = Timer.builder("ftgo.deliveries.pickup.duration") + .description("Time taken from assignment to pickup") + .tag("domain", "courier") + .register(registry); + + deliveryDistanceSummary = DistributionSummary.builder("ftgo.deliveries.distance") + .description("Distribution of delivery distances in kilometers") + .tag("domain", "courier") + .baseUnit("km") + .register(registry); + + Gauge.builder("ftgo.couriers.available", availableCouriers, AtomicLong::doubleValue) + .description("Current number of available couriers") + .tag("domain", "courier") + .register(registry); + } + + public void incrementDeliveriesCompleted() { + deliveriesCompletedCounter.increment(); + } + + public void incrementDeliveriesFailed() { + deliveriesFailedCounter.increment(); + } + + public void incrementCouriersCreated() { + couriersCreatedCounter.increment(); + } + + public Timer getDeliveryDurationTimer() { + return deliveryDurationTimer; + } + + public Timer getDeliveryPickupTimer() { + return deliveryPickupTimer; + } + + public void recordDeliveryDistance(double distanceKm) { + deliveryDistanceSummary.record(distanceKm); + } + + public void setAvailableCouriers(long count) { + availableCouriers.set(count); + } +} diff --git a/libs/ftgo-observability/src/main/java/com/ftgo/observability/metrics/OrderMetrics.java b/libs/ftgo-observability/src/main/java/com/ftgo/observability/metrics/OrderMetrics.java new file mode 100644 index 00000000..2204ddb8 --- /dev/null +++ b/libs/ftgo-observability/src/main/java/com/ftgo/observability/metrics/OrderMetrics.java @@ -0,0 +1,89 @@ +package com.ftgo.observability.metrics; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import jakarta.annotation.PostConstruct; + +public class OrderMetrics { + + private final MeterRegistry registry; + + private Counter ordersCreatedCounter; + private Counter ordersApprovedCounter; + private Counter ordersRejectedCounter; + private Counter ordersCancelledCounter; + private Counter ordersRevisedCounter; + private Timer orderFulfillmentTimer; + private Timer orderApprovalTimer; + + public OrderMetrics(MeterRegistry registry) { + this.registry = registry; + } + + @PostConstruct + public void init() { + ordersCreatedCounter = Counter.builder("ftgo.orders.created") + .description("Total number of orders created") + .tag("domain", "order") + .register(registry); + + ordersApprovedCounter = Counter.builder("ftgo.orders.approved") + .description("Total number of orders approved") + .tag("domain", "order") + .register(registry); + + ordersRejectedCounter = Counter.builder("ftgo.orders.rejected") + .description("Total number of orders rejected") + .tag("domain", "order") + .register(registry); + + ordersCancelledCounter = Counter.builder("ftgo.orders.cancelled") + .description("Total number of orders cancelled") + .tag("domain", "order") + .register(registry); + + ordersRevisedCounter = Counter.builder("ftgo.orders.revised") + .description("Total number of orders revised") + .tag("domain", "order") + .register(registry); + + orderFulfillmentTimer = Timer.builder("ftgo.orders.fulfillment.duration") + .description("Time taken to fulfill an order from creation to delivery") + .tag("domain", "order") + .register(registry); + + orderApprovalTimer = Timer.builder("ftgo.orders.approval.duration") + .description("Time taken from order creation to approval") + .tag("domain", "order") + .register(registry); + } + + public void incrementOrdersCreated() { + ordersCreatedCounter.increment(); + } + + public void incrementOrdersApproved() { + ordersApprovedCounter.increment(); + } + + public void incrementOrdersRejected() { + ordersRejectedCounter.increment(); + } + + public void incrementOrdersCancelled() { + ordersCancelledCounter.increment(); + } + + public void incrementOrdersRevised() { + ordersRevisedCounter.increment(); + } + + public Timer getOrderFulfillmentTimer() { + return orderFulfillmentTimer; + } + + public Timer getOrderApprovalTimer() { + return orderApprovalTimer; + } +} diff --git a/libs/ftgo-observability/src/main/java/com/ftgo/observability/metrics/RestaurantMetrics.java b/libs/ftgo-observability/src/main/java/com/ftgo/observability/metrics/RestaurantMetrics.java new file mode 100644 index 00000000..4760dc02 --- /dev/null +++ b/libs/ftgo-observability/src/main/java/com/ftgo/observability/metrics/RestaurantMetrics.java @@ -0,0 +1,81 @@ +package com.ftgo.observability.metrics; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import jakarta.annotation.PostConstruct; +import java.util.concurrent.atomic.AtomicLong; + +public class RestaurantMetrics { + + private final MeterRegistry registry; + + private Counter restaurantsCreatedCounter; + private Counter menuRevisionsCounter; + private Counter ticketsAcceptedCounter; + private Counter ticketsRejectedCounter; + private Timer ticketPreparationTimer; + private final AtomicLong activeRestaurants = new AtomicLong(0); + + public RestaurantMetrics(MeterRegistry registry) { + this.registry = registry; + } + + @PostConstruct + public void init() { + restaurantsCreatedCounter = Counter.builder("ftgo.restaurants.created") + .description("Total number of restaurants created") + .tag("domain", "restaurant") + .register(registry); + + menuRevisionsCounter = Counter.builder("ftgo.restaurants.menu.revisions") + .description("Total number of menu revisions") + .tag("domain", "restaurant") + .register(registry); + + ticketsAcceptedCounter = Counter.builder("ftgo.tickets.accepted") + .description("Total number of tickets accepted by restaurants") + .tag("domain", "restaurant") + .register(registry); + + ticketsRejectedCounter = Counter.builder("ftgo.tickets.rejected") + .description("Total number of tickets rejected by restaurants") + .tag("domain", "restaurant") + .register(registry); + + ticketPreparationTimer = Timer.builder("ftgo.tickets.preparation.duration") + .description("Time taken for ticket preparation") + .tag("domain", "restaurant") + .register(registry); + + Gauge.builder("ftgo.restaurants.active", activeRestaurants, AtomicLong::doubleValue) + .description("Current number of active restaurants") + .tag("domain", "restaurant") + .register(registry); + } + + public void incrementRestaurantsCreated() { + restaurantsCreatedCounter.increment(); + } + + public void incrementMenuRevisions() { + menuRevisionsCounter.increment(); + } + + public void incrementTicketsAccepted() { + ticketsAcceptedCounter.increment(); + } + + public void incrementTicketsRejected() { + ticketsRejectedCounter.increment(); + } + + public Timer getTicketPreparationTimer() { + return ticketPreparationTimer; + } + + public void setActiveRestaurants(long count) { + activeRestaurants.set(count); + } +} diff --git a/libs/ftgo-observability/src/main/resources/META-INF/ftgo-observability-defaults.properties b/libs/ftgo-observability/src/main/resources/META-INF/ftgo-observability-defaults.properties new file mode 100644 index 00000000..a618498d --- /dev/null +++ b/libs/ftgo-observability/src/main/resources/META-INF/ftgo-observability-defaults.properties @@ -0,0 +1,9 @@ +management.endpoints.web.exposure.include=health,info,prometheus,metrics +management.endpoint.health.show-details=when-authorized +management.endpoint.health.show-components=when-authorized +management.endpoint.prometheus.enabled=true +management.metrics.export.prometheus.enabled=true +management.metrics.distribution.percentiles-histogram.http.server.requests=true +management.metrics.distribution.percentiles.http.server.requests=0.5,0.9,0.95,0.99 +management.metrics.distribution.slo.http.server.requests=50ms,100ms,200ms,500ms,1s,5s +management.metrics.tags.application=${spring.application.name:unknown} diff --git a/libs/ftgo-observability/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/libs/ftgo-observability/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..a8dac05a --- /dev/null +++ b/libs/ftgo-observability/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +com.ftgo.observability.config.PrometheusMetricsAutoConfiguration +com.ftgo.observability.config.ActuatorEndpointConfiguration +com.ftgo.observability.config.BusinessMetricsAutoConfiguration diff --git a/libs/ftgo-observability/src/test/java/com/ftgo/observability/config/PrometheusMetricsAutoConfigurationTest.java b/libs/ftgo-observability/src/test/java/com/ftgo/observability/config/PrometheusMetricsAutoConfigurationTest.java new file mode 100644 index 00000000..955f70f1 --- /dev/null +++ b/libs/ftgo-observability/src/test/java/com/ftgo/observability/config/PrometheusMetricsAutoConfigurationTest.java @@ -0,0 +1,57 @@ +package com.ftgo.observability.config; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +class PrometheusMetricsAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(PrometheusMetricsAutoConfiguration.class)) + .withUserConfiguration(TestMeterRegistryConfig.class); + + @Test + void shouldCreateJvmMetricsBeans() { + contextRunner.run(context -> { + assertThat(context).hasSingleBean(io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics.class); + assertThat(context).hasSingleBean(io.micrometer.core.instrument.binder.jvm.JvmGcMetrics.class); + assertThat(context).hasSingleBean(io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics.class); + assertThat(context).hasSingleBean(io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics.class); + }); + } + + @Test + void shouldCreateSystemMetricsBeans() { + contextRunner.run(context -> { + assertThat(context).hasSingleBean(io.micrometer.core.instrument.binder.system.ProcessorMetrics.class); + assertThat(context).hasSingleBean(io.micrometer.core.instrument.binder.system.UptimeMetrics.class); + }); + } + + @Test + void shouldApplyCommonTags() { + contextRunner + .withPropertyValues( + "spring.application.name=test-service", + "ftgo.observability.application-name=test-service", + "ftgo.observability.environment=test" + ) + .run(context -> { + assertThat(context).hasSingleBean(MeterRegistry.class); + }); + } + + @Configuration + static class TestMeterRegistryConfig { + @Bean + MeterRegistry meterRegistry() { + return new SimpleMeterRegistry(); + } + } +} diff --git a/libs/ftgo-observability/src/test/java/com/ftgo/observability/metrics/ConsumerMetricsTest.java b/libs/ftgo-observability/src/test/java/com/ftgo/observability/metrics/ConsumerMetricsTest.java new file mode 100644 index 00000000..09e8f1ef --- /dev/null +++ b/libs/ftgo-observability/src/test/java/com/ftgo/observability/metrics/ConsumerMetricsTest.java @@ -0,0 +1,43 @@ +package com.ftgo.observability.metrics; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ConsumerMetricsTest { + + private MeterRegistry registry; + private ConsumerMetrics consumerMetrics; + + @BeforeEach + void setUp() { + registry = new SimpleMeterRegistry(); + consumerMetrics = new ConsumerMetrics(registry); + consumerMetrics.init(); + } + + @Test + void shouldIncrementConsumersRegisteredCounter() { + consumerMetrics.incrementConsumersRegistered(); + consumerMetrics.incrementConsumersRegistered(); + + assertThat(registry.counter("ftgo.consumers.registered", "domain", "consumer").count()).isEqualTo(2.0); + } + + @Test + void shouldIncrementConsumerVerificationsCounter() { + consumerMetrics.incrementConsumerVerifications(); + + assertThat(registry.counter("ftgo.consumers.verifications", "domain", "consumer").count()).isEqualTo(1.0); + } + + @Test + void shouldTrackActiveConsumersGauge() { + consumerMetrics.setActiveConsumers(42); + + assertThat(registry.get("ftgo.consumers.active").gauge().value()).isEqualTo(42.0); + } +} diff --git a/libs/ftgo-observability/src/test/java/com/ftgo/observability/metrics/CourierMetricsTest.java b/libs/ftgo-observability/src/test/java/com/ftgo/observability/metrics/CourierMetricsTest.java new file mode 100644 index 00000000..06ba4a61 --- /dev/null +++ b/libs/ftgo-observability/src/test/java/com/ftgo/observability/metrics/CourierMetricsTest.java @@ -0,0 +1,55 @@ +package com.ftgo.observability.metrics; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class CourierMetricsTest { + + private MeterRegistry registry; + private CourierMetrics courierMetrics; + + @BeforeEach + void setUp() { + registry = new SimpleMeterRegistry(); + courierMetrics = new CourierMetrics(registry); + courierMetrics.init(); + } + + @Test + void shouldIncrementDeliveriesCompletedCounter() { + courierMetrics.incrementDeliveriesCompleted(); + + assertThat(registry.counter("ftgo.deliveries.completed", "domain", "courier").count()).isEqualTo(1.0); + } + + @Test + void shouldIncrementDeliveriesFailedCounter() { + courierMetrics.incrementDeliveriesFailed(); + + assertThat(registry.counter("ftgo.deliveries.failed", "domain", "courier").count()).isEqualTo(1.0); + } + + @Test + void shouldTrackAvailableCouriersGauge() { + courierMetrics.setAvailableCouriers(15); + + assertThat(registry.get("ftgo.couriers.available").gauge().value()).isEqualTo(15.0); + } + + @Test + void shouldRecordDeliveryDistance() { + courierMetrics.recordDeliveryDistance(5.5); + courierMetrics.recordDeliveryDistance(3.2); + + assertThat(registry.summary("ftgo.deliveries.distance", "domain", "courier").count()).isEqualTo(2); + } + + @Test + void shouldProvideDeliveryDurationTimer() { + assertThat(courierMetrics.getDeliveryDurationTimer()).isNotNull(); + } +} diff --git a/libs/ftgo-observability/src/test/java/com/ftgo/observability/metrics/OrderMetricsTest.java b/libs/ftgo-observability/src/test/java/com/ftgo/observability/metrics/OrderMetricsTest.java new file mode 100644 index 00000000..19bd2134 --- /dev/null +++ b/libs/ftgo-observability/src/test/java/com/ftgo/observability/metrics/OrderMetricsTest.java @@ -0,0 +1,62 @@ +package com.ftgo.observability.metrics; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class OrderMetricsTest { + + private MeterRegistry registry; + private OrderMetrics orderMetrics; + + @BeforeEach + void setUp() { + registry = new SimpleMeterRegistry(); + orderMetrics = new OrderMetrics(registry); + orderMetrics.init(); + } + + @Test + void shouldIncrementOrdersCreatedCounter() { + orderMetrics.incrementOrdersCreated(); + orderMetrics.incrementOrdersCreated(); + + assertThat(registry.counter("ftgo.orders.created", "domain", "order").count()).isEqualTo(2.0); + } + + @Test + void shouldIncrementOrdersApprovedCounter() { + orderMetrics.incrementOrdersApproved(); + + assertThat(registry.counter("ftgo.orders.approved", "domain", "order").count()).isEqualTo(1.0); + } + + @Test + void shouldIncrementOrdersRejectedCounter() { + orderMetrics.incrementOrdersRejected(); + + assertThat(registry.counter("ftgo.orders.rejected", "domain", "order").count()).isEqualTo(1.0); + } + + @Test + void shouldIncrementOrdersCancelledCounter() { + orderMetrics.incrementOrdersCancelled(); + + assertThat(registry.counter("ftgo.orders.cancelled", "domain", "order").count()).isEqualTo(1.0); + } + + @Test + void shouldProvideOrderFulfillmentTimer() { + assertThat(orderMetrics.getOrderFulfillmentTimer()).isNotNull(); + assertThat(registry.timer("ftgo.orders.fulfillment.duration", "domain", "order")).isNotNull(); + } + + @Test + void shouldProvideOrderApprovalTimer() { + assertThat(orderMetrics.getOrderApprovalTimer()).isNotNull(); + assertThat(registry.timer("ftgo.orders.approval.duration", "domain", "order")).isNotNull(); + } +} diff --git a/libs/ftgo-observability/src/test/java/com/ftgo/observability/metrics/RestaurantMetricsTest.java b/libs/ftgo-observability/src/test/java/com/ftgo/observability/metrics/RestaurantMetricsTest.java new file mode 100644 index 00000000..c654585d --- /dev/null +++ b/libs/ftgo-observability/src/test/java/com/ftgo/observability/metrics/RestaurantMetricsTest.java @@ -0,0 +1,55 @@ +package com.ftgo.observability.metrics; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class RestaurantMetricsTest { + + private MeterRegistry registry; + private RestaurantMetrics restaurantMetrics; + + @BeforeEach + void setUp() { + registry = new SimpleMeterRegistry(); + restaurantMetrics = new RestaurantMetrics(registry); + restaurantMetrics.init(); + } + + @Test + void shouldIncrementRestaurantsCreatedCounter() { + restaurantMetrics.incrementRestaurantsCreated(); + + assertThat(registry.counter("ftgo.restaurants.created", "domain", "restaurant").count()).isEqualTo(1.0); + } + + @Test + void shouldIncrementMenuRevisionsCounter() { + restaurantMetrics.incrementMenuRevisions(); + restaurantMetrics.incrementMenuRevisions(); + + assertThat(registry.counter("ftgo.restaurants.menu.revisions", "domain", "restaurant").count()).isEqualTo(2.0); + } + + @Test + void shouldIncrementTicketsAcceptedCounter() { + restaurantMetrics.incrementTicketsAccepted(); + + assertThat(registry.counter("ftgo.tickets.accepted", "domain", "restaurant").count()).isEqualTo(1.0); + } + + @Test + void shouldTrackActiveRestaurantsGauge() { + restaurantMetrics.setActiveRestaurants(100); + + assertThat(registry.get("ftgo.restaurants.active").gauge().value()).isEqualTo(100.0); + } + + @Test + void shouldProvideTicketPreparationTimer() { + assertThat(restaurantMetrics.getTicketPreparationTimer()).isNotNull(); + } +}