diff --git a/README.md b/README.md
index eb24f63..60394c1 100644
--- a/README.md
+++ b/README.md
@@ -10,9 +10,10 @@ Instead of assuming a database scales, we **choose the DB**, implement the schem
## Modules & Status
-| Module | Description | Key Tech | Status |
-|:-----------------------------------------------|:--------------------------------------------------|:--------------------------|:-------------------|
-| **[Common Libraries](java/dsl-common)** (Java) | Shared utility libraries for distributed systems. | Distributed ID Generator | ✅ **Completed** |
-| **[URL Shortener](java/url-shortener)** | Distributed URL Shortener service | Netty, ScyllaDB | ✅ **Completed** |
-| **[Rate Limiter](java/rate-limiter)** | Distributed sliding window rate limiter. | Redis (Lua), Token Bucket | 🚧 **In Progress** |
+| Module | Description | Key Tech | Status |
+|:-----------------------------------------------|:---------------------------------------------------------|:----------------------------------|:----------------------|
+| **[Common Libraries](java/dsl-common)** (Java) | Shared utility libraries for distributed systems. | Distributed ID Generator | ✅ **Completed** |
+| **[Observability (lib)](java/dsl-observability)** | Central monitoring infrastructure & shared metrics lib. | Prometheus, Grafana | ✅ **Completed** |
+| **[URL Shortener](java/url-shortener)** | Distributed URL Shortener service | Netty, ScyllaDB | ✅ **Completed** |
+| **[Rate Limiter](java/rate-limiter)** | Distributed sliding window rate limiter. | Redis (Lua), Token Bucket | 🚧 **In Progress** |
diff --git a/docs/assets/observability/prometheus_targets.png b/docs/assets/observability/prometheus_targets.png
new file mode 100644
index 0000000..7c42def
Binary files /dev/null and b/docs/assets/observability/prometheus_targets.png differ
diff --git a/java/distributed-systems-lab.iml b/java/distributed-systems-lab.iml
new file mode 100644
index 0000000..c963b2a
--- /dev/null
+++ b/java/distributed-systems-lab.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/java/dsl-observability/README.md b/java/dsl-observability/README.md
new file mode 100644
index 0000000..ca963b5
--- /dev/null
+++ b/java/dsl-observability/README.md
@@ -0,0 +1,79 @@
+# DSL Observability & Infrastructure
+
+This module serves as the **Central Nervous System** for the Distributed Systems Lab. It provides a unified observability stack and shared infrastructure networking, ensuring all microservices report metrics in a consistent, standardized format.
+
+## Purpose
+1. **Infrastructure Hub:** Hosts the central **Prometheus** and **Grafana** containers and manages the shared Docker network (`dsl-net`).
+2. **Shared Library:** A lightweight Java library that configures **Micrometer** with standard tags (`env`, `service`, `region`), ensuring data consistency across all services.
+
+## Architecture
+
+The system follows a **Pull-Based** monitoring architecture. Applications expose a `/metrics` endpoint, and the central Prometheus server scrapes them over the shared internal network.
+
+```mermaid
+%%{init: {'theme': 'neutral'} }%%
+graph TD
+ subgraph "Shared Network (dsl-net)"
+ Service1[Service 1]
+ Service2[Service 2]
+ Service3[Service 3]
+
+ Prometheus[Prometheus]
+ Grafana[Grafana]
+ end
+
+ Service1 -- "Exposes /metrics" --> Prometheus
+ Service2 -- "Exposes /metrics" --> Prometheus
+ Service3 -- "Exposes /metrics" --> Prometheus
+ Prometheus -- "Data Source" --> Grafana
+```
+## Run This First!
+Since this module manages the shared network (`dsl-net`), you must start this stack before running any other microservice.
+```Bash
+ docker-compose up -d
+```
+1. Boot the Infrastructure
+ - Creates the `dsl-net` bridge network.
+ - Starts Prometheus (Port `9090`).
+ - Starts Grafana (Port `3000`).
+
+2. Access Dashboards
+ - Grafana: http://localhost:3000
+ - User: `admin`
+ - Password: `admin`
+ - Prometheus Target Status: http://localhost:9090/targets
+
+## Integration Guide
+To add observability to a new microservice, follow these steps:
+
+### 1. Add Maven Dependency
+Add the module to your `pom.xml`. This transitively pulls in `micrometer-registry-prometheus`.
+```xml
+
+ com.dsl
+ dsl-observability
+ 1.0-SNAPSHOT
+
+```
+
+### 2. Use the Shared Registry
+Instead of creating a new registry, use the singleton instance. This ensures standard tags are applied automatically.
+```java
+private final PrometheusMeterRegistry registry = DslMetrics.getInstance();
+```
+
+### 3. Expose the Endpoint
+Your HTTP server must expose the scraped data.
+
+### 4. Register in Prometheus
+Add your service to prometheus.yml:
+```yml
+scrape_configs:
+ - job_name: 'my-new-service'
+ metrics_path: '/metrics'
+ static_configs:
+ - targets: ['container-name:port']
+```
+
+## Prometheus Target Health Dashboard
+
diff --git a/java/dsl-observability/docker-compose.yml b/java/dsl-observability/docker-compose.yml
new file mode 100644
index 0000000..9ba1952
--- /dev/null
+++ b/java/dsl-observability/docker-compose.yml
@@ -0,0 +1,25 @@
+services:
+ prometheus:
+ image: prom/prometheus:latest
+ container_name: dsl-prometheus
+ volumes:
+ - ./prometheus.yml:/etc/prometheus/prometheus.yml
+ ports:
+ - "9090:9090"
+ networks:
+ - dsl-net
+
+ grafana:
+ image: grafana/grafana:latest
+ container_name: dsl-grafana
+ ports:
+ - "3000:3000"
+ environment:
+ - GF_SECURITY_ADMIN_PASSWORD=admin
+ networks:
+ - dsl-net
+
+networks:
+ dsl-net:
+ name: dsl-net
+ driver: bridge
\ No newline at end of file
diff --git a/java/dsl-observability/pom.xml b/java/dsl-observability/pom.xml
new file mode 100644
index 0000000..0d00127
--- /dev/null
+++ b/java/dsl-observability/pom.xml
@@ -0,0 +1,28 @@
+
+
+ 4.0.0
+
+
+
+ com.dsl
+ distributed-systems-lab
+ 1.0-SNAPSHOT
+
+
+ com.dsl.observability
+ dsl-observability
+
+
+ 1.16.0
+
+
+
+
+ io.micrometer
+ micrometer-registry-prometheus
+ ${micrometer-prometheus.version}
+
+
+
\ No newline at end of file
diff --git a/java/dsl-observability/prometheus.yml b/java/dsl-observability/prometheus.yml
new file mode 100644
index 0000000..5392a66
--- /dev/null
+++ b/java/dsl-observability/prometheus.yml
@@ -0,0 +1,15 @@
+global:
+ scrape_interval: 5s
+
+scrape_configs:
+ # Scrape the URL Shortener
+ - job_name: 'url-shortener'
+ metrics_path: '/metrics'
+ static_configs:
+ - targets: ['url-shortener-app:8080']
+
+ # Scrape the Rate Limiter
+ - job_name: 'rate-limiter'
+ metrics_path: '/metrics'
+ static_configs:
+ - targets: ['rate-limiter-app:8081']
\ No newline at end of file
diff --git a/java/dsl-observability/src/main/java/com/dsl/observability/DslMetrics.java b/java/dsl-observability/src/main/java/com/dsl/observability/DslMetrics.java
new file mode 100644
index 0000000..70a4f26
--- /dev/null
+++ b/java/dsl-observability/src/main/java/com/dsl/observability/DslMetrics.java
@@ -0,0 +1,24 @@
+package com.dsl.observability;
+
+import io.micrometer.prometheusmetrics.PrometheusConfig;
+import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
+
+public class DslMetrics {
+
+ private static volatile PrometheusMeterRegistry registry;
+
+ private DslMetrics() {
+ // Prevent instantiation
+ }
+
+ public static PrometheusMeterRegistry getInstance() {
+ if (registry == null) {
+ synchronized (DslMetrics.class) {
+ if (registry == null) {
+ registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
+ }
+ }
+ }
+ return registry;
+ }
+}
\ No newline at end of file
diff --git a/java/pom.xml b/java/pom.xml
index 7bc37dd..2d51fd7 100644
--- a/java/pom.xml
+++ b/java/pom.xml
@@ -10,6 +10,7 @@
dsl-common
+ dsl-observability
url-shortener
rate-limiter
diff --git a/java/rate-limiter/docker-compose.yml b/java/rate-limiter/docker-compose.yml
index e1a34aa..4b9833a 100644
--- a/java/rate-limiter/docker-compose.yml
+++ b/java/rate-limiter/docker-compose.yml
@@ -1,5 +1,3 @@
-version: '3.8'
-
services:
# 1. Redis
redis:
@@ -7,6 +5,8 @@ services:
container_name: limiter-redis
ports:
- "6379:6379"
+ networks:
+ - dsl-net
# 2. Rate Limiter App
app:
@@ -15,8 +15,15 @@ services:
context: ..
dockerfile: rate-limiter/Dockerfile
ports:
- - "8080:8080"
+ - "8081:8081"
depends_on:
- redis
environment:
- - REDIS_HOST=redis
\ No newline at end of file
+ - REDIS_HOST=redis
+ - PORT=8081
+ networks:
+ - dsl-net
+
+networks:
+ dsl-net:
+ external: true
\ No newline at end of file
diff --git a/java/rate-limiter/pom.xml b/java/rate-limiter/pom.xml
index f7044b9..4306743 100644
--- a/java/rate-limiter/pom.xml
+++ b/java/rate-limiter/pom.xml
@@ -22,6 +22,12 @@
+
+ com.dsl.observability
+ dsl-observability
+ 1.0-SNAPSHOT
+
+
io.lettuce
lettuce-core
diff --git a/java/rate-limiter/src/main/java/com/dsl/ratelimiter/Application.java b/java/rate-limiter/src/main/java/com/dsl/ratelimiter/Application.java
index 9f4ae70..ccbc7c4 100644
--- a/java/rate-limiter/src/main/java/com/dsl/ratelimiter/Application.java
+++ b/java/rate-limiter/src/main/java/com/dsl/ratelimiter/Application.java
@@ -6,11 +6,13 @@
import com.dsl.ratelimiter.strategy.RateLimiterStrategy;
import io.lettuce.core.RedisClient;
import io.lettuce.core.api.StatefulRedisConnection;
+import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
+import com.dsl.observability.DslMetrics;
public class Application {
public static void main(String[] args) throws Exception {
- int port = 8080;
+ int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8081"));
String redisHost = System.getenv().getOrDefault("REDIS_HOST", "localhost");
String redisUrl = "redis://" + redisHost + ":6379";
@@ -22,10 +24,11 @@ public static void main(String[] args) throws Exception {
RateLimiterFactory factory = new RateLimiterFactory(connection.sync());
RateLimiterStrategy rateLimiter = factory.get(RateLimiterFactory.Type.TOKEN_BUCKET);
+ PrometheusMeterRegistry meterRegistry = DslMetrics.getInstance();
System.out.println("Token Bucket Strategy Loaded.");
- NettyServer server = new NettyServer<>(port, new RateLimitHandler(rateLimiter));
+ NettyServer server = new NettyServer<>(port, new RateLimitHandler(rateLimiter, meterRegistry));
server.start();
}
}
diff --git a/java/rate-limiter/src/main/java/com/dsl/ratelimiter/handler/RateLimitHandler.java b/java/rate-limiter/src/main/java/com/dsl/ratelimiter/handler/RateLimitHandler.java
index 25df2eb..f2bfa75 100644
--- a/java/rate-limiter/src/main/java/com/dsl/ratelimiter/handler/RateLimitHandler.java
+++ b/java/rate-limiter/src/main/java/com/dsl/ratelimiter/handler/RateLimitHandler.java
@@ -1,27 +1,47 @@
package com.dsl.ratelimiter.handler;
import com.dsl.ratelimiter.strategy.RateLimiterStrategy;
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.Timer;
+import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
+import java.time.Duration;
+
@ChannelHandler.Sharable
public class RateLimitHandler extends SimpleChannelInboundHandler {
private final RateLimiterStrategy strategy;
private final long capacity;
private final long rate;
+ private final PrometheusMeterRegistry meterRegistry;
+ private final Counter allowedCounter;
+ private final Counter blockedCounter;
+ private final Timer checkTimer;
- public RateLimitHandler(RateLimiterStrategy strategy) {
+ public RateLimitHandler(RateLimiterStrategy strategy, PrometheusMeterRegistry registry) {
this.strategy = strategy;
+ this.meterRegistry = registry;
+ this.allowedCounter = registry.counter("ratelimiter.requests", "result", "allowed");
+ this.blockedCounter = registry.counter("ratelimiter.requests", "result", "blocked");
+ this.checkTimer = registry.timer("ratelimiter.check.duration");
this.capacity = 10;
this.rate = 1;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) {
+ String uri = req.uri();
+
+ if ("/metrics".equals(uri)) {
+ sendResponse(ctx, HttpResponseStatus.OK, meterRegistry.scrape());
+ return;
+ }
+
String userId = "anonymous";
if (req.headers().contains("X-User-ID")) {
userId = req.headers().get("X-User-ID");
@@ -29,11 +49,15 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) {
String key = "rate_limit:" + userId;
+ long start = System.nanoTime();
boolean allowed = strategy.isAllowed(key, capacity, rate);
+ checkTimer.record(Duration.ofNanos(System.nanoTime() - start));
if (allowed) {
+ allowedCounter.increment();
sendResponse(ctx, HttpResponseStatus.OK, "Request Allowed for " + userId);
} else {
+ blockedCounter.increment();
sendResponse(ctx, HttpResponseStatus.TOO_MANY_REQUESTS, "Rate Limit Exceeded");
}
}
diff --git a/java/url-shortener/README.md b/java/url-shortener/README.md
index 7d6ee59..2866084 100644
--- a/java/url-shortener/README.md
+++ b/java/url-shortener/README.md
@@ -65,6 +65,8 @@ A high-throughput, low-latency URL Shortener service built from scratch in Java.
This spins up the App, ScyllaDB, Prometheus, and Grafana as docker containers.
```bash
+ cd java/dsl-observability
+ docker-compose up --build
cd java/url-shortener
docker-compose up --build
```
diff --git a/java/url-shortener/docker-compose.yml b/java/url-shortener/docker-compose.yml
index a84abe3..1aae919 100644
--- a/java/url-shortener/docker-compose.yml
+++ b/java/url-shortener/docker-compose.yml
@@ -1,5 +1,3 @@
-version: '3.8'
-
services:
scylla:
image: scylladb/scylla:5.2
@@ -10,6 +8,8 @@ services:
command: --smp 1 --memory 750M --overprovisioned 1 --api-address 0.0.0.0
ports:
- "9042:9042"
+ networks:
+ - dsl-net
volumes:
- scylla-data:/var/lib/scylla
healthcheck:
@@ -18,11 +18,11 @@ services:
timeout: 10s
retries: 10
- url-shortener:
+ app:
build:
context: ..
dockerfile: url-shortener/Dockerfile
- container_name: url-shortener
+ container_name: url-shortener-app
ports:
- "8080:8080"
environment:
@@ -32,26 +32,12 @@ services:
depends_on:
scylla:
condition: service_healthy
-
- prometheus:
- image: prom/prometheus:latest
- container_name: shortener-prom
- volumes:
- - ./prometheus.yml:/etc/prometheus/prometheus.yml
- ports:
- - "9090:9090"
- depends_on:
- - url-shortener
-
- grafana:
- image: grafana/grafana:latest
- container_name: shortener-grafana
- ports:
- - "3000:3000"
- environment:
- - GF_SECURITY_ADMIN_PASSWORD=admin
- depends_on:
- - prometheus
+ networks:
+ - dsl-net
volumes:
- scylla-data:
\ No newline at end of file
+ scylla-data:
+
+networks:
+ dsl-net:
+ external: true
\ No newline at end of file
diff --git a/java/url-shortener/pom.xml b/java/url-shortener/pom.xml
index f0c94b2..94af817 100644
--- a/java/url-shortener/pom.xml
+++ b/java/url-shortener/pom.xml
@@ -29,6 +29,12 @@
1.0-SNAPSHOT
+
+ com.dsl.observability
+ dsl-observability
+ 1.0-SNAPSHOT
+
+
io.netty
netty-codec-http
@@ -46,12 +52,6 @@
slf4j-simple
${slf4j.version}
-
-
- io.micrometer
- micrometer-registry-prometheus
- ${micrometer-prometheus.version}
-
diff --git a/java/url-shortener/prometheus.yml b/java/url-shortener/prometheus.yml
deleted file mode 100644
index 1406f0a..0000000
--- a/java/url-shortener/prometheus.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-global:
- scrape_interval: 5s
-
-scrape_configs:
- - job_name: 'url-shortener'
- metrics_path: '/metrics'
- static_configs:
- - targets: ['url-shortener:8080']
\ No newline at end of file
diff --git a/java/url-shortener/src/main/java/com/dsl/urlshortener/Application.java b/java/url-shortener/src/main/java/com/dsl/urlshortener/Application.java
index c78ead4..2b1791f 100644
--- a/java/url-shortener/src/main/java/com/dsl/urlshortener/Application.java
+++ b/java/url-shortener/src/main/java/com/dsl/urlshortener/Application.java
@@ -2,11 +2,11 @@
import com.datastax.oss.driver.api.core.CqlSession;
import com.dsl.common.idgenerator.SnowflakeIdGenerator;
+import com.dsl.observability.DslMetrics;
import com.dsl.urlshortener.handler.ShortenerHandler;
import com.dsl.urlshortener.repository.ScyllaUrlRepository;
import com.dsl.urlshortener.repository.UrlRepository;
import com.dsl.urlshortener.server.NettyServer;
-import io.micrometer.prometheusmetrics.PrometheusConfig;
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
import java.net.InetSocketAddress;
@@ -38,7 +38,7 @@ public static void main(String[] args) throws Exception {
UrlRepository repository = new ScyllaUrlRepository(session);
- PrometheusMeterRegistry meterRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
+ PrometheusMeterRegistry meterRegistry = DslMetrics.getInstance();
ShortenerHandler shortenerHandler = new ShortenerHandler(idGenerator, repository, host, meterRegistry);