commit edb7a69bde7233bc2e96b5d1520e0ff237babb4b Author: domenico Date: Fri Oct 24 20:55:45 2025 +0200 commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..90df72b --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# WellD challenge report + +## What I did + +- **Analyzed the existing Spring Boot/Thymeleaf order management application** for missing metrics. +- **Added and exposed additional custom metrics** using Micrometer and Spring Boot Actuator, making them available at `/actuator/prometheus` for Prometheus scraping. +- **Implemented the following custom metrics:** + - `orders_deleted_total`: Total orders deleted. + - `orders_created_per_product_total` (with `product` tag): Orders created per product. + - `order_quantity_average`: Distribution summary for order quantities (enables average, max, count, sum such as `order_quantity_average_sum` and `order_quantity_average_total`). + - `log_events_total` (with `level` tag): Counts of INFO and ERROR log events. +- **Ensured JVM and HTTP metrics are available** (latency, memory, CPU, threads, GC, etc.) via Actuator. +- **Created a Grafana dashboard** (see `grafana/monitoring/`) with panels for all key metrics and business KPIs. + +--- + +## How to use the dashboard + +1. Build the source code with `mvn clean package` + +2. Start the stack: `docker compose up -d` + +3. Access Prometheus at http://localhost:9090 + +4. Access Grafana at http://localhost:3000 (default login: `admin:admin`) + +5. Import the JSON dashboard provided in this repository under `grafana/monitoring/` + +6. Interact with the application at http://localhost:8080/web/orders , metrics will update in realtime on the dashboard. + + +--- + +## Which panels were created and their purpose + +- **Total Orders Created:** + Visualizes the cumulative number of orders created (`orders_created_total`). +- **Total Orders Deleted:** + Shows the number of orders deleted (`orders_deleted_total`). +- **Orders per Product:** + Bar chart/table using `orders_created_per_product_total{product="..."}` for business insight. +- **Average Quantity per Order:** + Displays the average order quantity using `order_quantity_average_sum / order_quantity_average_count`. +- **HTTP Request Latency (p95, p99):** + Shows high-percentile request durations per endpoint using: + `histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket[5m])) by (le, uri))` + and + `histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket[5m])) by (le, uri))` +- **JVM Memory & CPU Usage:** + Monitors resource usage with `jvm_memory_used_bytes`, `process_cpu_usage`, etc. +- **Log Event Counters:** + Visualizes counts of INFO and ERROR logs (`log_events_total{level="INFO"}` and `log_events_total{level="ERROR"}`). +- **Other Stability Metrics:** + Panels for thread count, GC pause time, and queue size. + + +## Proposed alerts and thresholds + +1. **High HTTP Latency**, threshold of `p95 > 1s for 5m`, to detect slow endpoints + +2. **High ERROR log rate**, threshold of `>5 errors/min`, might indicate bugs or failures + +3. **High JVM memory usage**, threshold of `>80% for 5m`, might help to prevent OOM errors + +4. **High CPU usage**, threshold of `>80% for 5m`, might help to detect resource exhaustion + +--- + +## Custom metrics and their purpose + +- **orders_deleted_total:** Tracks deletions for auditing and anomaly detection. +- **orders_created_per_product_total (with optional `product` tag):** Enables product-level business insights and anomaly detection. +- **order_quantity_average:** Monitors average order size, useful for business KPIs and detecting outliers. +- **log_events_total (with `level` tag):** Provides visibility into application health and error rates. + diff --git a/grafana/monitoring/WellD-challenge-1761329840417.json b/grafana/monitoring/WellD-challenge-1761329840417.json new file mode 100644 index 0000000..1fcbed6 --- /dev/null +++ b/grafana/monitoring/WellD-challenge-1761329840417.json @@ -0,0 +1,1321 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "panels": [], + "title": "Orders", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 5, + "x": 0, + "y": 1 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "orders_created_total", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Total orders created", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 5, + "y": 1 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "orders_deleted_total", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Total orders deleted", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 5, + "x": 11, + "y": 1 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "editorMode": "code", + "expr": "order_quantity_average_sum / order_quantity_average_count", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "avg quantity per order", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 7, + "x": 16, + "y": 1 + }, + "id": 12, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "editorMode": "code", + "expr": "orders_created_per_product_total", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Orders per product (total)", + "type": "timeseries" + }, + { + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 16, + "title": "Other stats", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 7, + "x": 0, + "y": 9 + }, + "id": 14, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "editorMode": "code", + "expr": "jvm_gc_overhead_percent", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "JVM GC overhead %", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 7, + "y": 9 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "editorMode": "code", + "expr": "log_events_total{level=\"ERROR\"}", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Event logs (ERROR)", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 3, + "panels": [], + "title": "System stats", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 7, + "x": 0, + "y": 17 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "editorMode": "code", + "expr": "log_events_total{level=\"INFO\"}", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Events log (INFO)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 5, + "x": 7, + "y": 17 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "editorMode": "code", + "expr": "jvm_threads_live_threads\n", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "JVM threads", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 11, + "x": 12, + "y": 17 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "editorMode": "code", + "expr": "jvm_memory_used_bytes/1024/1024", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "JVM memory used (MB)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 0, + "y": 25 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket[5m])) by (le, uri, method))\n", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "http req latency (p95)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 4, + "y": 25 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket[5m])) by (le, uri, method))", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "http req latency (p99)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 7, + "x": 8, + "y": 25 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "editorMode": "code", + "expr": "process_cpu_usage", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Process CPU %", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 15, + "y": 25 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "df21ddv7y86bkf" + }, + "editorMode": "code", + "expr": "system_cpu_usage", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "System CPU %", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "2025-10-24T17:22:53.995Z", + "to": "2025-10-24T18:08:10.560Z" + }, + "timepicker": {}, + "timezone": "browser", + "title": "WellD challenge", + "uid": "cf21fnif55ypsc", + "version": 4, + "weekStart": "" +} \ No newline at end of file diff --git a/wellDMonitoringChallenge-main/.gitignore b/wellDMonitoringChallenge-main/.gitignore new file mode 100644 index 0000000..0cfb56a --- /dev/null +++ b/wellDMonitoringChallenge-main/.gitignore @@ -0,0 +1,49 @@ +# Exclude grafana and target directories +grafana/ +target/ + +# Maven +*.class +*.jar +*.war +*.ear +*.zip +*.tar.gz +*.rar +hs_err_pid* +replay_pid* + +# IDE files +.idea/ +*.iml +.vscode/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +*.log +logs/ + +# Temporary files +*.tmp +*.temp + +# Docker +.dockerignore + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local \ No newline at end of file diff --git a/wellDMonitoringChallenge-main/Dockerfile b/wellDMonitoringChallenge-main/Dockerfile new file mode 100644 index 0000000..a99d1da --- /dev/null +++ b/wellDMonitoringChallenge-main/Dockerfile @@ -0,0 +1,9 @@ +FROM eclipse-temurin:17.0.15_6-jre-ubi9-minimal + +WORKDIR /app + +COPY target/monitoring-test-0.0.1-SNAPSHOT.jar app.jar + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/wellDMonitoringChallenge-main/README.md b/wellDMonitoringChallenge-main/README.md new file mode 100644 index 0000000..ef95f82 --- /dev/null +++ b/wellDMonitoringChallenge-main/README.md @@ -0,0 +1,125 @@ +# Candidate Assessment Project + +## Overview + +You will work on a **Java Spring Boot web application** with a **Thymeleaf frontend** for managing orders. + +The application includes: + +- REST endpoints (`/api/orders`) and web endpoints (`/web/orders`) +- Order creation with `id`, `productName`, and `quantity` +- Viewing and deleting orders via forms +- Metrics exposed via Spring Boot Actuator at `/actuator/prometheus` +- Application logging configured for INFO, ERROR, and DEBUG levels + +--- + +## Environment + +The **pre-configured environment is provided** with: + +- The **Java application,** **Prometheus** and **Grafana** already set up inside `docker-compose.yml`. +- Prometheus scraping configurations for the application's `/actuator/prometheus` endpoint ready. + +You can start the environment (after minimal manipulation) with: + +```bash +docker-compose up +``` + +N.B. +Grafana is to be configured to use Prometheus as its data source. + +--- + +## Task + +### Monitoring and Dashboard Creation + +Your task is to **create a Grafana dashboard** to monitor this application using **Prometheus** as the data source. + +The dashboard should include: + +- Total orders created +- Total orders deleted +- HTTP request latency (p95, p99) +- JVM memory and CPU usage +- Logged events count (INFO, ERROR) +- Any additional metrics you consider relevant for application stability and debugging + +### Custom Metrics + +As part of this assessment, you are **required to create and expose custom metrics** in the application. Examples of custom metrics you could implement: + +- Number of orders per product +- Average quantity per order +- Custom application counters or timers useful for business KPIs + +These custom metrics should be: + +- Exposed via Prometheus in the application +- Documented in the deliverables +- Visualized in the Grafana dashboard + +### Brief Observability Strategy + +In addition to the dashboard: + +- Describe which **alerts** you would configure, including thresholds and the rationale behind them. + +--- + +## Optional Improvements + +You are welcome to implement additional improvements, such as: + +- Adding **tags to metrics** for richer filtering +- Improving **structured logging** to trace order flows +- Tools and approaches for **profiling and identifying performance bottlenecks** + +--- + +## Deliverables + +- Create a **Git repository**. +- Add the **Grafana dashboard JSON** ready to import under a `grafana/monitoring/` folder. +- Add your updated **Java application with custom metrics** exposed. +- Create a `README.md` describing: + - What you did + - How to use the dashboard + - Which panels were created and their purpose + - Proposed alerts and thresholds + - The custom metrics you created and their purpose + +--- + +## Requirements + +- Java 17 +- Maven +- Docker and Docker Compose + +--- + +## Objective + +This assessment aims to evaluate your ability to: + +- Configure effective **monitoring** on a Java web application +- Select and create **meaningful metrics** for observability and debugging +- Implement **custom Prometheus metrics** for business and technical insights +- Communicate clearly through documentation +- Apply **DevOps practices** for production stability +- Find and correct anti-patterns in the configuration + +--- + +## Contact + +For technical questions, please contact: + +[attilio.gualandi@welld.ch] + +--- + +Thank you for your time and effort on this assessment. diff --git a/wellDMonitoringChallenge-main/docker-compose.yaml b/wellDMonitoringChallenge-main/docker-compose.yaml new file mode 100644 index 0000000..5ecbacb --- /dev/null +++ b/wellDMonitoringChallenge-main/docker-compose.yaml @@ -0,0 +1,34 @@ +version: '3.8' + +services: + monitoring-app: + image: monitoring-test:latest + build: + context: . + ports: + - "8080:8080" + networks: + - monitoring + + prometheus: + image: prom/prometheus:v2.52.0 + ports: + - "9090:9090" + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + networks: + - monitoring + + grafana: + image: grafana/grafana:10.4.2 + ports: + - "3000:3000" + networks: + - monitoring + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin + +networks: + monitoring: + driver: bridge \ No newline at end of file diff --git a/wellDMonitoringChallenge-main/pom.xml b/wellDMonitoringChallenge-main/pom.xml new file mode 100644 index 0000000..aad6208 --- /dev/null +++ b/wellDMonitoringChallenge-main/pom.xml @@ -0,0 +1,57 @@ + + 4.0.0 + + com.example + monitoring-test + 0.0.1-SNAPSHOT + jar + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + Monitoring Test App + Spring Boot application exposing Prometheus metrics for Grafana testing + + + 17 + + monitoring-test + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-actuator + + + + io.micrometer + micrometer-registry-prometheus + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/wellDMonitoringChallenge-main/prometheus/prometheus.yml b/wellDMonitoringChallenge-main/prometheus/prometheus.yml new file mode 100644 index 0000000..db0f0c8 --- /dev/null +++ b/wellDMonitoringChallenge-main/prometheus/prometheus.yml @@ -0,0 +1,8 @@ +global: + scrape_interval: 5s + +scrape_configs: + - job_name: 'monitoring-app' + metrics_path: '/actuator/prometheus' + static_configs: + - targets: ['monitoring-app:8080'] \ No newline at end of file diff --git a/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/MonitoringTestApplication.java b/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/MonitoringTestApplication.java new file mode 100644 index 0000000..4d4e492 --- /dev/null +++ b/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/MonitoringTestApplication.java @@ -0,0 +1,20 @@ +package com.example.monitoringtest; + +import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import io.micrometer.core.instrument.MeterRegistry; + +@SpringBootApplication +public class MonitoringTestApplication { + + public static void main(String[] args) { + SpringApplication.run(MonitoringTestApplication.class, args); + } + + @Bean + MeterRegistryCustomizer metricsCommonTags() { + return registry -> registry.config().commonTags("application", "monitoring-test-app"); + } +} \ No newline at end of file diff --git a/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/controller/MonitoringController.java b/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/controller/MonitoringController.java new file mode 100644 index 0000000..a1118cc --- /dev/null +++ b/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/controller/MonitoringController.java @@ -0,0 +1,41 @@ +package com.example.monitoringtest.controller; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.concurrent.atomic.AtomicInteger; + +@RestController +class MonitoringController { + + private final Counter helloCounter; + private final AtomicInteger queueSize; + + public MonitoringController(MeterRegistry registry) { + this.helloCounter = Counter.builder("app_requests_hello_total") + .description("Total requests to /hello endpoint") + .register(registry); + + this.queueSize = new AtomicInteger(0); + Gauge.builder("app_queue_size", queueSize, AtomicInteger::get) + .description("Current size of the processing queue") + .register(registry); + } + + @GetMapping("/hello") + public String hello() { + helloCounter.increment(); + // Simulate queue increase + queueSize.incrementAndGet(); + return "Hello, Monitoring Engineer!"; + } + + @GetMapping("/dequeue") + public String dequeue() { + queueSize.updateAndGet(size -> size > 0 ? size - 1 : 0); + return "Dequeued one item. Current queue size: " + queueSize.get(); + } +} \ No newline at end of file diff --git a/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/controller/OrderController.java b/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/controller/OrderController.java new file mode 100644 index 0000000..e230a5d --- /dev/null +++ b/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/controller/OrderController.java @@ -0,0 +1,54 @@ +package com.example.monitoringtest.controller; + +import com.example.monitoringtest.model.Order; +import com.example.monitoringtest.service.OrderService; +import com.example.monitoringtest.metrics.OrderMetrics; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/orders") +public class OrderController { + + private final OrderService orderService; + private final OrderMetrics orderMetrics; + + public OrderController(OrderService orderService, OrderMetrics orderMetrics) { + this.orderService = orderService; + this.orderMetrics = orderMetrics; + } + + @PostMapping + public ResponseEntity createOrder(@RequestBody Order order) { + Order created = orderService.createOrder(order); + orderMetrics.incrementOrderCount(); + orderMetrics.recordOrderQuantity(order.getQuantity()); + orderMetrics.incrementOrderPerProduct(order.getProductName()); + return ResponseEntity.ok(created); + } + + @GetMapping + public ResponseEntity> getOrders() { + List orders = orderService.getAllOrders(); + return ResponseEntity.ok(orders); + } + + @GetMapping("/{id}") + public ResponseEntity getOrderById(@PathVariable Long id) { + try { + Order order = orderService.getOrderById(id); + return ResponseEntity.ok(order); + } catch (RuntimeException e) { + return ResponseEntity.notFound().build(); + } + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteOrder(@PathVariable Long id) { + orderService.deleteOrder(id); + orderMetrics.incrementOrderDeletedCount(); + return ResponseEntity.noContent().build(); + } +} \ No newline at end of file diff --git a/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/controller/WebOrderController.java b/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/controller/WebOrderController.java new file mode 100644 index 0000000..3de8ea0 --- /dev/null +++ b/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/controller/WebOrderController.java @@ -0,0 +1,57 @@ +package com.example.monitoringtest.controller; + +import com.example.monitoringtest.model.Order; +import com.example.monitoringtest.service.OrderService; +import com.example.monitoringtest.metrics.OrderMetrics; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; + + +@Controller +@RequestMapping("/web/orders") +public class WebOrderController { + + private final OrderService orderService; + private final OrderMetrics orderMetrics; + + + public WebOrderController(OrderService orderService, OrderMetrics orderMetrics) { + this.orderService = orderService; + this.orderMetrics = orderMetrics; + } + + @GetMapping("/test") + @ResponseBody + public String test() { + System.out.println("WebOrderController.test() called"); + return "Test endpoint working!"; + } + + @GetMapping + public String getOrders(Model model) { + System.out.println("WebOrderController.getOrders() called"); + System.out.println("Returning view name: order-list"); + model.addAttribute("orders", orderService.getAllOrders()); + model.addAttribute("order", new Order()); + return "order-list"; + } + + @PostMapping + public String createOrder(@ModelAttribute Order order) { + orderService.createOrder(order); + orderMetrics.incrementOrderCount(); + orderMetrics.recordOrderQuantity(order.getQuantity()); + orderMetrics.incrementOrderPerProduct(order.getProductName()); + return "redirect:/web/orders"; + } + + @PostMapping("/{id}") + public String deleteOrder(@PathVariable Long id, @RequestParam("_method") String method) { + if ("delete".equalsIgnoreCase(method)) { + orderService.deleteOrder(id); + orderMetrics.incrementOrderDeletedCount(); + } + return "redirect:/web/orders"; + } +} \ No newline at end of file diff --git a/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/metrics/LogMetrics.java b/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/metrics/LogMetrics.java new file mode 100644 index 0000000..37b6318 --- /dev/null +++ b/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/metrics/LogMetrics.java @@ -0,0 +1,24 @@ +package com.example.monitoringtest.metrics; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.stereotype.Component; + +@Component +public class LogMetrics { + private final Counter infoCounter; + private final Counter errorCounter; + + public LogMetrics(MeterRegistry meterRegistry) { + this.infoCounter = meterRegistry.counter("log_events_total", "level", "INFO"); + this.errorCounter = meterRegistry.counter("log_events_total", "level", "ERROR"); + } + + public void incrementInfo() { + infoCounter.increment(); + } + + public void incrementError() { + errorCounter.increment(); + } +} diff --git a/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/metrics/MetricsLogAppender.java b/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/metrics/MetricsLogAppender.java new file mode 100644 index 0000000..d4c8ef4 --- /dev/null +++ b/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/metrics/MetricsLogAppender.java @@ -0,0 +1,28 @@ +package com.example.monitoringtest.metrics; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class MetricsLogAppender extends AppenderBase { + + private static LogMetrics logMetrics; + + @Autowired + public void setLogMetrics(LogMetrics logMetrics) { + MetricsLogAppender.logMetrics = logMetrics; + } + + @Override + protected void append(ILoggingEvent eventObject) { + if (logMetrics == null) return; + if (eventObject.getLevel() == Level.INFO) { + logMetrics.incrementInfo(); + } else if (eventObject.getLevel() == Level.ERROR) { + logMetrics.incrementError(); + } + } +} diff --git a/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/metrics/OrderMetrics.java b/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/metrics/OrderMetrics.java new file mode 100644 index 0000000..064c75a --- /dev/null +++ b/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/metrics/OrderMetrics.java @@ -0,0 +1,42 @@ +package com.example.monitoringtest.metrics; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.stereotype.Component; + +import io.micrometer.core.instrument.DistributionSummary; + + +@Component +public class OrderMetrics { + + private final Counter orderCounter; + private final Counter orderDeletedCounter; + private final MeterRegistry meterRegistry; + private final DistributionSummary orderQuantityAverage; + + + public OrderMetrics(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + this.orderCounter = meterRegistry.counter("orders_created_total"); + this.orderDeletedCounter = meterRegistry.counter("orders_deleted_total"); + this.orderQuantityAverage = DistributionSummary.builder("order_quantity_average").description("Order quantity average").register(meterRegistry); + } + + public void incrementOrderCount() { + orderCounter.increment(); + } + + public void incrementOrderDeletedCount() { + orderDeletedCounter.increment(); + } + + public void incrementOrderPerProduct(String productName) { + meterRegistry.counter("orders_created_per_product_total", "product", productName).increment(); + } + + public void recordOrderQuantity(Integer quantity) { + orderQuantityAverage.record(quantity); + } + +} \ No newline at end of file diff --git a/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/model/Order.java b/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/model/Order.java new file mode 100644 index 0000000..5a1440e --- /dev/null +++ b/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/model/Order.java @@ -0,0 +1,23 @@ +package com.example.monitoringtest.model; + +public class Order { + private Long id; + private String productName; + private Integer quantity; + + public Order() {} + + public Order(Long id, String productName, Integer quantity) { + this.id = id; + this.productName = productName; + this.quantity = quantity; + } + + // Getters and setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getProductName() { return productName; } + public void setProductName(String productName) { this.productName = productName; } + public Integer getQuantity() { return quantity; } + public void setQuantity(Integer quantity) { this.quantity = quantity; } +} \ No newline at end of file diff --git a/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/repository/OrderRepository.java b/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/repository/OrderRepository.java new file mode 100644 index 0000000..927ecff --- /dev/null +++ b/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/repository/OrderRepository.java @@ -0,0 +1,33 @@ +package com.example.monitoringtest.repository; + +import com.example.monitoringtest.model.Order; +import org.springframework.stereotype.Repository; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +@Repository +public class OrderRepository { + private final Map orders = new ConcurrentHashMap<>(); + private final AtomicLong counter = new AtomicLong(1); + + public Order save(Order order) { + Long id = counter.getAndIncrement(); + order.setId(id); + orders.put(id, order); + return order; + } + + public List findAll() { + return new ArrayList<>(orders.values()); + } + + public Optional findById(Long id) { + return Optional.ofNullable(orders.get(id)); + } + + public void deleteById(Long id) { + orders.remove(id); + } +} \ No newline at end of file diff --git a/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/service/OrderService.java b/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/service/OrderService.java new file mode 100644 index 0000000..bbe606d --- /dev/null +++ b/wellDMonitoringChallenge-main/src/main/java/com/example/monitoringtest/service/OrderService.java @@ -0,0 +1,34 @@ +package com.example.monitoringtest.service; + +import com.example.monitoringtest.model.Order; +import com.example.monitoringtest.repository.OrderRepository; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class OrderService { + + private final OrderRepository repository; + + public OrderService(OrderRepository repository) { + this.repository = repository; + } + + public Order createOrder(Order order) { + return repository.save(order); + } + + public List getAllOrders() { + return repository.findAll(); + } + + public Order getOrderById(Long id) { + return repository.findById(id) + .orElseThrow(() -> new RuntimeException("Order not found with id: " + id)); + } + + public void deleteOrder(Long id) { + repository.deleteById(id); + } +} \ No newline at end of file diff --git a/wellDMonitoringChallenge-main/src/main/resources/application.properties b/wellDMonitoringChallenge-main/src/main/resources/application.properties new file mode 100644 index 0000000..5d72c48 --- /dev/null +++ b/wellDMonitoringChallenge-main/src/main/resources/application.properties @@ -0,0 +1,8 @@ +management.endpoints.web.exposure.include=health,info,prometheus +management.endpoint.prometheus.enabled=true +management.metrics.distribution.percentiles-histogram.http.server.requests=true + +# Thymeleaf configuration +spring.thymeleaf.cache=false +logging.level.org.thymeleaf=INFO +logging.level.org.springframework.web=INFO \ No newline at end of file diff --git a/wellDMonitoringChallenge-main/src/main/resources/logback-spring.xml b/wellDMonitoringChallenge-main/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..34a024c --- /dev/null +++ b/wellDMonitoringChallenge-main/src/main/resources/logback-spring.xml @@ -0,0 +1,12 @@ + + + + + %d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/wellDMonitoringChallenge-main/src/main/resources/static/favicon.svg b/wellDMonitoringChallenge-main/src/main/resources/static/favicon.svg new file mode 100644 index 0000000..34e2faa --- /dev/null +++ b/wellDMonitoringChallenge-main/src/main/resources/static/favicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/wellDMonitoringChallenge-main/src/main/resources/templates/order-list.html b/wellDMonitoringChallenge-main/src/main/resources/templates/order-list.html new file mode 100644 index 0000000..4c9ad85 --- /dev/null +++ b/wellDMonitoringChallenge-main/src/main/resources/templates/order-list.html @@ -0,0 +1,186 @@ + + + + + + Order Management + + + + +

Order Management

+ +

Orders count: 0

+ +

Create New Order

+
+ Product Name:
+ Quantity:
+ +
+ +

Current Orders

+
+

No orders found.

+
+ + + + + + + + + + + + + + + + + +
IDProduct NameQuantityAction
+
+ + +
+
+ + \ No newline at end of file