Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** |

Binary file added docs/assets/observability/prometheus_targets.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions java/distributed-systems-lab.iml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$" dumb="true">
<sourceFolder url="file://$MODULE_DIR$/dsl-observability/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/rate-limiter/src/main/java" isTestSource="false" />
</content>
</component>
</module>
79 changes: 79 additions & 0 deletions java/dsl-observability/README.md
Original file line number Diff line number Diff line change
@@ -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
<dependency>
<groupId>com.dsl</groupId>
<artifactId>dsl-observability</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
```

### 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)
25 changes: 25 additions & 0 deletions java/dsl-observability/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions java/dsl-observability/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>

<!-- Reference to the parent POM -->
<parent>
<groupId>com.dsl</groupId>
<artifactId>distributed-systems-lab</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<groupId>com.dsl.observability</groupId>
<artifactId>dsl-observability</artifactId>

<properties>
<micrometer-prometheus.version>1.16.0</micrometer-prometheus.version>
</properties>

<dependencies>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>${micrometer-prometheus.version}</version>
</dependency>
</dependencies>
</project>
15 changes: 15 additions & 0 deletions java/dsl-observability/prometheus.yml
Original file line number Diff line number Diff line change
@@ -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']
Original file line number Diff line number Diff line change
@@ -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;
}
}
1 change: 1 addition & 0 deletions java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

<modules>
<module>dsl-common</module>
<module>dsl-observability</module>
<module>url-shortener</module>
<module>rate-limiter</module>
</modules>
Expand Down
15 changes: 11 additions & 4 deletions java/rate-limiter/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
version: '3.8'

services:
# 1. Redis
redis:
image: redis:alpine
container_name: limiter-redis
ports:
- "6379:6379"
networks:
- dsl-net

# 2. Rate Limiter App
app:
Expand All @@ -15,8 +15,15 @@ services:
context: ..
dockerfile: rate-limiter/Dockerfile
ports:
- "8080:8080"
- "8081:8081"
depends_on:
- redis
environment:
- REDIS_HOST=redis
- REDIS_HOST=redis
- PORT=8081
networks:
- dsl-net

networks:
dsl-net:
external: true
6 changes: 6 additions & 0 deletions java/rate-limiter/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
</properties>

<dependencies>
<dependency>
<groupId>com.dsl.observability</groupId>
<artifactId>dsl-observability</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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<RateLimitHandler> server = new NettyServer<>(port, new RateLimitHandler(rateLimiter));
NettyServer<RateLimitHandler> server = new NettyServer<>(port, new RateLimitHandler(rateLimiter, meterRegistry));
server.start();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,63 @@
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<FullHttpRequest> {

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");
}

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");
}
}
Expand Down
2 changes: 2 additions & 0 deletions java/url-shortener/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
Loading