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 +![prometheus_targets.png](../../docs/assets/observability/prometheus_targets.png) 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);