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