commit
This commit is contained in:
75
README.md
Normal file
75
README.md
Normal file
@@ -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.
|
||||||
|
|
||||||
1321
grafana/monitoring/WellD-challenge-1761329840417.json
Normal file
1321
grafana/monitoring/WellD-challenge-1761329840417.json
Normal file
File diff suppressed because it is too large
Load Diff
49
wellDMonitoringChallenge-main/.gitignore
vendored
Normal file
49
wellDMonitoringChallenge-main/.gitignore
vendored
Normal file
@@ -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
|
||||||
9
wellDMonitoringChallenge-main/Dockerfile
Normal file
9
wellDMonitoringChallenge-main/Dockerfile
Normal file
@@ -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"]
|
||||||
125
wellDMonitoringChallenge-main/README.md
Normal file
125
wellDMonitoringChallenge-main/README.md
Normal file
@@ -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.
|
||||||
34
wellDMonitoringChallenge-main/docker-compose.yaml
Normal file
34
wellDMonitoringChallenge-main/docker-compose.yaml
Normal file
@@ -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
|
||||||
57
wellDMonitoringChallenge-main/pom.xml
Normal file
57
wellDMonitoringChallenge-main/pom.xml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>com.example</groupId>
|
||||||
|
<artifactId>monitoring-test</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.2.5</version>
|
||||||
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<name>Monitoring Test App</name>
|
||||||
|
<description>Spring Boot application exposing Prometheus metrics for Grafana testing</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>17</java.version>
|
||||||
|
|
||||||
|
<docker.image.prefix>monitoring-test</docker.image.prefix>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.micrometer</groupId>
|
||||||
|
<artifactId>micrometer-registry-prometheus</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
8
wellDMonitoringChallenge-main/prometheus/prometheus.yml
Normal file
8
wellDMonitoringChallenge-main/prometheus/prometheus.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
global:
|
||||||
|
scrape_interval: 5s
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: 'monitoring-app'
|
||||||
|
metrics_path: '/actuator/prometheus'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['monitoring-app:8080']
|
||||||
@@ -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<MeterRegistry> metricsCommonTags() {
|
||||||
|
return registry -> registry.config().commonTags("application", "monitoring-test-app");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Order> 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<List<Order>> getOrders() {
|
||||||
|
List<Order> orders = orderService.getAllOrders();
|
||||||
|
return ResponseEntity.ok(orders);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<Order> 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<Void> deleteOrder(@PathVariable Long id) {
|
||||||
|
orderService.deleteOrder(id);
|
||||||
|
orderMetrics.incrementOrderDeletedCount();
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ILoggingEvent> {
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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<Long, Order> 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<Order> findAll() {
|
||||||
|
return new ArrayList<>(orders.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Order> findById(Long id) {
|
||||||
|
return Optional.ofNullable(orders.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteById(Long id) {
|
||||||
|
orders.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Order> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<configuration>
|
||||||
|
<appender name="METRICS" class="com.example.monitoringtest.metrics.MetricsLogAppender"/>
|
||||||
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
<root level="INFO">
|
||||||
|
<appender-ref ref="METRICS"/>
|
||||||
|
<appender-ref ref="CONSOLE"/>
|
||||||
|
</root>
|
||||||
|
</configuration>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">
|
||||||
|
<rect x="4" y="4" width="56" height="56" rx="12" fill="rgba(0,0,0,0.8)"/>
|
||||||
|
<polyline points="20,34 28,42 44,26" fill="none" stroke="white" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 278 B |
@@ -0,0 +1,186 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&family=Roboto+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||||
|
<title>Order Management</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: "Poppins","Segoe UI", "Roboto", sans-serif;
|
||||||
|
background: linear-gradient(135deg, #000000, #222222);
|
||||||
|
color: #f0f0f0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 2rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2 {
|
||||||
|
color: #ffffff;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: italic;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-style: italic;
|
||||||
|
color: #dddddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
padding: 0.5rem 2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.6);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="number"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #ffffff;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: #aaaaaa;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background 0.3s ease, transform 0.2s ease;
|
||||||
|
width: 100%;
|
||||||
|
font-style: italic;
|
||||||
|
letter-spacing: 0.8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-create {
|
||||||
|
background: rgba(0, 128, 255, 0.2);
|
||||||
|
color: #8ccfff;
|
||||||
|
border: 1px solid rgba(140, 207, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-create:hover {
|
||||||
|
background: rgba(0, 128, 255, 0.35);
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete {
|
||||||
|
background: rgba(255, 0, 0, 0.15);
|
||||||
|
color: #ff6b6b;
|
||||||
|
border: 1px solid rgba(255, 107, 107, 0.3);
|
||||||
|
width: auto;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete:hover {
|
||||||
|
background: rgba(255, 0, 0, 0.3);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 800px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.6);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 0.6rem 0.8rem;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #ffffff;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-orders {
|
||||||
|
font-style: italic;
|
||||||
|
color: #cccccc;
|
||||||
|
padding: 1rem;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
|
||||||
|
max-width: 400px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-cell {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>Order Management</h1>
|
||||||
|
|
||||||
|
<p>Orders count: <span th:text="${orders.size()}">0</span></p>
|
||||||
|
|
||||||
|
<h2>Create New Order</h2>
|
||||||
|
<form action="/web/orders" method="post">
|
||||||
|
Product Name: <input type="text" name="productName" required placeholder="Enter product name"/><br/>
|
||||||
|
Quantity: <input type="number" name="quantity" required placeholder="Enter quantity"/><br/>
|
||||||
|
<button type="submit" class="btn-create">Create Order</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h2>Current Orders</h2>
|
||||||
|
<div th:if="${orders.empty}" class="no-orders">
|
||||||
|
<p>No orders found.</p>
|
||||||
|
</div>
|
||||||
|
<table border="0" th:if="${!orders.empty}">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Product Name</th>
|
||||||
|
<th>Quantity</th>
|
||||||
|
<th class="action-cell">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="order : ${orders}">
|
||||||
|
<td th:text="${order.id}"></td>
|
||||||
|
<td th:text="${order.productName}"></td>
|
||||||
|
<td th:text="${order.quantity}"></td>
|
||||||
|
<td class="action-cell">
|
||||||
|
<form th:action="@{/web/orders/{id}(id=${order.id})}" method="post" style="display: inline;">
|
||||||
|
<input type="hidden" name="_method" value="delete"/>
|
||||||
|
<button type="submit" class="btn-delete">Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user