Created
November 20, 2025 15:51
-
-
Save muhdkhokhar/95690ea6bb86c8ba0f3c0128b17578b0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | |
| <modelVersion>4.0.0</modelVersion> | |
| <parent> | |
| <groupId>org.springframework.boot</groupId> | |
| <artifactId>spring-boot-starter-parent</artifactId> | |
| <version>3.2.0</version> | |
| <relativePath/> | |
| </parent> | |
| <groupId>com.example</groupId> | |
| <artifactId>chronicle-event-store</artifactId> | |
| <version>0.0.1-SNAPSHOT</version> | |
| <properties> | |
| <java.version>17</java.version> | |
| <chronicle.version>2.25.105</chronicle.version> | |
| </properties> | |
| <dependencies> | |
| <dependency> | |
| <groupId>org.springframework.boot</groupId> | |
| <artifactId>spring-boot-starter-web</artifactId> | |
| </dependency> | |
| <dependency> | |
| <groupId>net.openhft</groupId> | |
| <artifactId>chronicle-queue</artifactId> | |
| <version>${chronicle.version}</version> | |
| </dependency> | |
| <dependency> | |
| <groupId>net.openhft</groupId> | |
| <artifactId>chronicle-map</artifactId> | |
| <version>${chronicle.version}</version> | |
| </dependency> | |
| <dependency> | |
| <groupId>org.springframework.boot</groupId> | |
| <artifactId>spring-boot-starter-test</artifactId> | |
| <scope>test</scope> | |
| </dependency> | |
| </dependencies> | |
| <build> | |
| <plugins> | |
| <plugin> | |
| <groupId>org.springframework.boot</groupId> | |
| <artifactId>spring-boot-maven-plugin</artifactId> | |
| </plugin> | |
| </plugins> | |
| </build> | |
| </project> | |
| package com.example.chronicleeventstore; | |
| import net.openhft.chronicle.wire.SelfDescribingMarshallable; | |
| public class Event extends SelfDescribingMarshallable { | |
| public String eventId; // The main event ID (e.g., "001") | |
| public int stepNumber; // The sequence number | |
| public long timestamp; | |
| public String stepName; // e.g., "getdata" | |
| public String stepOutput; // The rich output (JSON, table data, etc.) | |
| // Standard getters/setters/constructors omitted for brevity, but needed in a real project | |
| // Chronicle only requires public fields or standard Java bean conventions | |
| } | |
| package com.example.chronicleeventstore; | |
| import net.openhft.chronicle.map.ChronicleMap; | |
| import net.openhft.chronicle.queue.ChronicleQueue; | |
| import net.openhft.chronicle.queue.ExcerptAppender; | |
| import net.openhft.chronicle.queue.ExcerptTailer; | |
| import org.springframework.beans.factory.annotation.Value; | |
| import org.springframework.stereotype.Service; | |
| import jakarta.annotation.PostConstruct; | |
| import jakarta.annotation.PreDestroy; | |
| import java.io.File; | |
| import java.io.IOException; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import java.util.UUID; | |
| @Service | |
| public class EventStorageService { | |
| @Value("${chronicle.queue.path:/tmp/chronicle-events}") | |
| private String queuePath; | |
| // Chronicle Components | |
| private ChronicleQueue queue; | |
| private ExcerptAppender appender; | |
| private ChronicleMap<String, List<Long>> eventIndexMap; | |
| @PostConstruct | |
| public void init() throws IOException { | |
| // 1. Initialize Chronicle Queue | |
| File queueDir = new File(queuePath); | |
| queueDir.mkdirs(); | |
| this.queue = ChronicleQueue.single(queueDir.getAbsolutePath()); | |
| this.appender = queue.acquireAppender(); | |
| // 2. Initialize Chronicle Map (Event Index) | |
| // Key: Event ID (String), Value: List of Queue Indices (Longs) | |
| this.eventIndexMap = ChronicleMap | |
| .of(String.class, (Class<List<Long>>) (Class<?>) List.class) | |
| .name("event-index-map") | |
| .averageKey("event-001") | |
| .averageValue(List.of(0L, 0L, 0L, 0L)) // Estimate for 4 steps per event | |
| .entries(10_000) | |
| .create(); | |
| System.out.println("Chronicle initialized. Queue at: " + queuePath); | |
| } | |
| // --- WRITE/INDEX METHOD --- | |
| public void saveStep(Event event) { | |
| // 1. Write the Step to Chronicle Queue | |
| appender.writeDocument(w -> w.writeMarshallable(event)); | |
| // 2. Capture the index (address) of the written entry | |
| long index = appender.lastIndex(); | |
| // 3. Update the Chronicle Map index | |
| // Use computeIfAbsent for a thread-safe way to get/create the list | |
| eventIndexMap.compute(event.eventId, (id, indices) -> { | |
| if (indices == null) { | |
| indices = new ArrayList<>(); | |
| } | |
| indices.add(index); | |
| return indices; | |
| }); | |
| System.out.printf("-> Wrote step %d for Event %s at index %d%n", event.stepNumber, event.eventId, index); | |
| } | |
| // --- READ/LOOKUP METHOD --- | |
| public List<Event> getEventHistory(String eventId) { | |
| // 1. Map Lookup: Get the list of indices for the Event ID | |
| List<Long> indices = eventIndexMap.get(eventId); | |
| if (indices == null || indices.isEmpty()) { | |
| return List.of(); | |
| } | |
| List<Event> history = new ArrayList<>(); | |
| ExcerptTailer tailer = queue.createTailer(); | |
| // 2. Queue Lookup: Jump directly to each index and read the data | |
| for (Long index : indices) { | |
| boolean success = tailer.moveToIndex(index); | |
| if (success) { | |
| Event event = tailer.readDocument(r -> r.readMarshallable(Event.class)); | |
| if (event != null) { | |
| history.add(event); | |
| } | |
| } | |
| } | |
| return history; | |
| } | |
| @PreDestroy | |
| public void close() { | |
| // Ensure resources are closed cleanly | |
| if (eventIndexMap != null) { | |
| eventIndexMap.close(); | |
| } | |
| if (queue != null) { | |
| queue.close(); | |
| } | |
| System.out.println("Chronicle resources closed."); | |
| } | |
| } | |
| package com.example.chronicleeventstore; | |
| import org.springframework.http.ResponseEntity; | |
| import org.springframework.web.bind.annotation.*; | |
| import java.util.List; | |
| import java.util.UUID; | |
| @RestController | |
| @RequestMapping("/api/events") | |
| public class EventController { | |
| private final EventStorageService storageService; | |
| public EventController(EventStorageService storageService) { | |
| this.storageService = storageService; | |
| } | |
| @PostMapping("/simulate") | |
| public ResponseEntity<String> simulateEvent() { | |
| String newEventId = "EVENT-" + UUID.randomUUID().toString().substring(0, 4); | |
| // --- Simulate Step 1: getData --- | |
| Event step1 = new Event(); | |
| step1.eventId = newEventId; | |
| step1.stepNumber = 1; | |
| step1.timestamp = System.currentTimeMillis(); | |
| step1.stepName = "getdata"; | |
| step1.stepOutput = "{\"query\":\"SELECT * FROM users;\", \"rows\":15}"; | |
| storageService.saveStep(step1); | |
| // --- Simulate Step 2: transform message --- | |
| Event step2 = new Event(); | |
| step2.eventId = newEventId; | |
| step2.stepNumber = 2; | |
| step2.timestamp = System.currentTimeMillis() + 50; | |
| step2.stepName = "transform message"; | |
| step2.stepOutput = "{\"status\":\"processed\",\"user_count\":15}"; | |
| storageService.saveStep(step2); | |
| // --- Simulate Step 3: send message to Kafka --- | |
| Event step3 = new Event(); | |
| step3.eventId = newEventId; | |
| step3.stepNumber = 3; | |
| step3.timestamp = System.currentTimeMillis() + 100; | |
| step3.stepName = "send message to Kafka"; | |
| step3.stepOutput = "topic: processed_events, partition: 2, offset: 12345"; | |
| storageService.saveStep(step3); | |
| return ResponseEntity.ok("Simulated new event: **" + newEventId + "** with 3 steps."); | |
| } | |
| @GetMapping("/{eventId}") | |
| public List<Event> getEventHistory(@PathVariable String eventId) { | |
| System.out.println("<- UI Request for Event ID: " + eventId); | |
| // This simulates the timeline request: Map lookup -> Queue lookup | |
| return storageService.getEventHistory(eventId); | |
| } | |
| } | |
| package com.example.chronicleeventstore; | |
| import org.springframework.boot.SpringApplication; | |
| import org.springframework.boot.autoconfigure.SpringBootApplication; | |
| @SpringBootApplication | |
| public class ChronicleApp { | |
| public static void main(String[] args) { | |
| SpringApplication.run(ChronicleApp.class, args); | |
| } | |
| } | |
| package com.example.chronicleeventstore; | |
| import org.springframework.http.ResponseEntity; | |
| import org.springframework.web.bind.annotation.*; | |
| import java.util.List; | |
| import java.util.UUID; | |
| @RestController | |
| @RequestMapping("/api/events") | |
| // Add CORS support so Angular running on port 4200 can connect | |
| @CrossOrigin(origins = "http://localhost:4200") | |
| public class EventController { | |
| private final EventStorageService storageService; | |
| public EventController(EventStorageService storageService) { | |
| this.storageService = storageService; | |
| } | |
| // ... (simulateEvent method is the same) | |
| @PostMapping("/simulate") | |
| public ResponseEntity<String> simulateEvent() { | |
| String newEventId = "EVENT-" + UUID.randomUUID().toString().substring(0, 4); | |
| // --- Simulate Step 1: getData --- | |
| Event step1 = new Event(); | |
| step1.eventId = newEventId; | |
| step1.stepNumber = 1; | |
| step1.timestamp = System.currentTimeMillis(); | |
| step1.stepName = "getdata"; | |
| step1.stepOutput = "{\"query\":\"SELECT * FROM users;\", \"rows\":15}"; | |
| storageService.saveStep(step1); | |
| // --- Simulate Step 2: transform message --- | |
| Event step2 = new Event(); | |
| step2.eventId = newEventId; | |
| step2.stepNumber = 2; | |
| step2.timestamp = System.currentTimeMillis() + 50; | |
| step2.stepName = "transform message"; | |
| step2.stepOutput = "{\"status\":\"processed\",\"user_count\":15}"; | |
| storageService.saveStep(step2); | |
| // --- Simulate Step 3: send message to Kafka --- | |
| Event step3 = new Event(); | |
| step3.eventId = newEventId; | |
| step3.stepNumber = 3; | |
| step3.timestamp = System.currentTimeMillis() + 100; | |
| step3.stepName = "send message to Kafka"; | |
| step3.stepOutput = "topic: processed_events, partition: 2, offset: 12345"; | |
| storageService.saveStep(step3); | |
| return ResponseEntity.ok(newEventId); // Return just the ID for the UI | |
| } | |
| // ... (getEventHistory method is the same) | |
| @GetMapping("/{eventId}") | |
| public List<Event> getEventHistory(@PathVariable String eventId) { | |
| System.out.println("<- UI Request for Event ID: " + eventId); | |
| return storageService.getEventHistory(eventId); | |
| } | |
| } | |
| import { Component } from '@angular/core'; | |
| import { HttpClient } from '@angular/common/http'; | |
| import { Event } from '../event.model'; | |
| import { map } from 'rxjs/operators'; | |
| @Component({ | |
| selector: 'app-timeline', | |
| templateUrl: './timeline.component.html', | |
| styleUrls: ['./timeline.component.css'] | |
| }) | |
| export class TimelineComponent { | |
| eventId: string = ''; | |
| eventHistory: Event[] = []; | |
| isLoading: boolean = false; | |
| errorMessage: string = ''; | |
| apiBaseUrl: string = 'http://localhost:8080/api/events'; | |
| constructor(private http: HttpClient) { } | |
| /** | |
| * Simulates a new event by calling the backend's POST endpoint. | |
| */ | |
| simulateEvent() { | |
| this.isLoading = true; | |
| this.errorMessage = ''; | |
| this.eventHistory = []; | |
| this.http.post(this.apiBaseUrl + '/simulate', {}, { responseType: 'text' }) | |
| .subscribe({ | |
| next: (id: string) => { | |
| this.eventId = id; | |
| console.log(`Simulated Event ID: ${id}`); | |
| // Automatically fetch the history of the new event | |
| this.fetchHistory(); | |
| }, | |
| error: (err) => { | |
| this.errorMessage = 'Failed to simulate event. Is the Spring Boot backend running?'; | |
| this.isLoading = false; | |
| } | |
| }); | |
| } | |
| /** | |
| * Fetches the event history for the entered Event ID. | |
| */ | |
| fetchHistory() { | |
| if (!this.eventId) { | |
| this.errorMessage = 'Please enter an Event ID or simulate a new event.'; | |
| return; | |
| } | |
| this.isLoading = true; | |
| this.errorMessage = ''; | |
| this.eventHistory = []; | |
| this.http.get<Event[]>(`${this.apiBaseUrl}/${this.eventId}`) | |
| .pipe( | |
| // Map the timestamp to a readable format for display | |
| map(events => events.map(event => ({ | |
| ...event, | |
| timestamp: new Date(event.timestamp).toLocaleString() as any // Overriding type for display | |
| }))) | |
| ) | |
| .subscribe({ | |
| next: (history) => { | |
| this.eventHistory = history; | |
| this.isLoading = false; | |
| if (history.length === 0) { | |
| this.errorMessage = `No history found for Event ID: ${this.eventId}`; | |
| } | |
| }, | |
| error: (err) => { | |
| this.errorMessage = 'Error fetching history. Check console for details.'; | |
| this.isLoading = false; | |
| } | |
| }); | |
| } | |
| } | |
| That's a great next step! To give you a complete, runnable example, I'll provide a structure for the backend (Spring Boot/Chronicle) and a simple frontend (Angular) that consumes the data and renders a basic timeline. | |
| Since I cannot directly create a ZIP file, I will provide the files organized into their respective folders. You can copy these files into a project structure and run them. | |
| 📦 Project Structure | |
| chronicle-event-store-project/ | |
| ├── backend/ | |
| │ ├── src/main/java/com/example/chronicleeventstore/ | |
| │ │ ├── ChronicleApp.java (Main Spring Boot App) | |
| │ │ ├── Event.java (The Step Data Model) | |
| │ │ ├── EventStorageService.java (CQ/CM Logic) | |
| │ │ └── EventController.java (REST API) | |
| │ └── pom.xml | |
| │ | |
| └── frontend/ | |
| ├── src/ | |
| │ ├── app/ | |
| │ │ ├── timeline/ | |
| │ │ │ ├── timeline.component.ts | |
| │ │ │ ├── timeline.component.html | |
| │ │ │ └── timeline.component.css | |
| │ │ └── app.component.html | |
| │ └── assets/ | |
| └── package.json | |
| 1. ☕ Backend (Spring Boot/Chronicle) | |
| The backend code remains largely the same as the previous response, but I've added a configuration to allow the Angular UI to connect (CORS). | |
| backend/src/main/java/.../EventController.java (Updated with CORS) | |
| Java | |
| package com.example.chronicleeventstore; | |
| import org.springframework.http.ResponseEntity; | |
| import org.springframework.web.bind.annotation.*; | |
| import java.util.List; | |
| import java.util.UUID; | |
| @RestController | |
| @RequestMapping("/api/events") | |
| // Add CORS support so Angular running on port 4200 can connect | |
| @CrossOrigin(origins = "http://localhost:4200") | |
| public class EventController { | |
| private final EventStorageService storageService; | |
| public EventController(EventStorageService storageService) { | |
| this.storageService = storageService; | |
| } | |
| // ... (simulateEvent method is the same) | |
| @PostMapping("/simulate") | |
| public ResponseEntity<String> simulateEvent() { | |
| String newEventId = "EVENT-" + UUID.randomUUID().toString().substring(0, 4); | |
| // --- Simulate Step 1: getData --- | |
| Event step1 = new Event(); | |
| step1.eventId = newEventId; | |
| step1.stepNumber = 1; | |
| step1.timestamp = System.currentTimeMillis(); | |
| step1.stepName = "getdata"; | |
| step1.stepOutput = "{\"query\":\"SELECT * FROM users;\", \"rows\":15}"; | |
| storageService.saveStep(step1); | |
| // --- Simulate Step 2: transform message --- | |
| Event step2 = new Event(); | |
| step2.eventId = newEventId; | |
| step2.stepNumber = 2; | |
| step2.timestamp = System.currentTimeMillis() + 50; | |
| step2.stepName = "transform message"; | |
| step2.stepOutput = "{\"status\":\"processed\",\"user_count\":15}"; | |
| storageService.saveStep(step2); | |
| // --- Simulate Step 3: send message to Kafka --- | |
| Event step3 = new Event(); | |
| step3.eventId = newEventId; | |
| step3.stepNumber = 3; | |
| step3.timestamp = System.currentTimeMillis() + 100; | |
| step3.stepName = "send message to Kafka"; | |
| step3.stepOutput = "topic: processed_events, partition: 2, offset: 12345"; | |
| storageService.saveStep(step3); | |
| return ResponseEntity.ok(newEventId); // Return just the ID for the UI | |
| } | |
| // ... (getEventHistory method is the same) | |
| @GetMapping("/{eventId}") | |
| public List<Event> getEventHistory(@PathVariable String eventId) { | |
| System.out.println("<- UI Request for Event ID: " + eventId); | |
| return storageService.getEventHistory(eventId); | |
| } | |
| } | |
| The other backend files (ChronicleApp.java, Event.java, EventStorageService.java, and pom.xml) are identical to the previous response. | |
| 2. 🎨 Frontend (Angular) | |
| This is a minimal Angular application that allows a user to input an Event ID and displays the result as a simple timeline. | |
| Setup Instructions (Assuming you have Node.js and Angular CLI installed) | |
| Navigate into the frontend directory. | |
| Run npm install to install dependencies. | |
| Run ng serve --open to start the application (it usually runs on http://localhost:4200). | |
| frontend/src/app/event.model.ts | |
| <div class="container"> | |
| <h2>Chronicle Event Timeline Viewer</h2> | |
| <div class="input-group"> | |
| <input | |
| type="text" | |
| [(ngModel)]="eventId" | |
| placeholder="Enter Event ID (e.g., EVENT-a8c3)"> | |
| <button (click)="fetchHistory()" [disabled]="isLoading"> | |
| {{ isLoading && eventId ? 'Loading...' : 'Load History' }} | |
| </button> | |
| <button (click)="simulateEvent()" [disabled]="isLoading"> | |
| {{ isLoading && !eventId ? 'Simulating...' : 'Simulate New Event' }} | |
| </button> | |
| </div> | |
| <div *ngIf="errorMessage" class="error-message">{{ errorMessage }}</div> | |
| <div *ngIf="eventHistory.length > 0"> | |
| <h3>Timeline for Event: <strong>{{ eventHistory[0].eventId }}</strong></h3> | |
| <div class="timeline"> | |
| <div *ngFor="let step of eventHistory" class="timeline-item"> | |
| <div class="timeline-dot"></div> | |
| <div class="timeline-content"> | |
| <h4>Step {{ step.stepNumber }}: {{ step.stepName | titlecase }}</h4> | |
| <p class="timestamp"> | |
| <small>Completed at: {{ step.timestamp }}</small> | |
| </p> | |
| <div class="output-panel"> | |
| <strong>Step Output:</strong> | |
| <pre>{{ step.stepOutput }}</pre> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| .container { | |
| max-width: 800px; | |
| margin: 40px auto; | |
| font-family: Arial, sans-serif; | |
| } | |
| .input-group { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| } | |
| .input-group input { | |
| flex-grow: 1; | |
| padding: 10px; | |
| border: 1px solid #ccc; | |
| border-radius: 4px; | |
| } | |
| .input-group button { | |
| padding: 10px 15px; | |
| background-color: #007bff; | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| } | |
| .timeline { | |
| position: relative; | |
| padding: 20px 0; | |
| margin-left: 20px; | |
| } | |
| /* Vertical line */ | |
| .timeline::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| bottom: 0; | |
| left: 0; | |
| width: 2px; | |
| background-color: #e0e0e0; | |
| } | |
| .timeline-item { | |
| position: relative; | |
| padding-left: 30px; | |
| margin-bottom: 30px; | |
| } | |
| .timeline-dot { | |
| position: absolute; | |
| top: 5px; | |
| left: -8px; | |
| width: 16px; | |
| height: 16px; | |
| background-color: #007bff; | |
| border-radius: 50%; | |
| border: 3px solid white; | |
| box-shadow: 0 0 0 2px #007bff; | |
| } | |
| .timeline-content { | |
| background: #f9f9f9; | |
| padding: 15px; | |
| border-radius: 6px; | |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); | |
| } | |
| .timestamp { | |
| color: #888; | |
| margin-top: 5px; | |
| } | |
| .output-panel { | |
| margin-top: 10px; | |
| padding: 10px; | |
| background-color: #eee; | |
| border-radius: 4px; | |
| } | |
| .output-panel pre { | |
| white-space: pre-wrap; | |
| word-break: break-all; | |
| font-size: 0.9em; | |
| margin: 0; | |
| } | |
| .error-message { | |
| padding: 10px; | |
| background-color: #f8d7da; | |
| color: #721c24; | |
| border: 1px solid #f5c6cb; | |
| border-radius: 4px; | |
| margin-bottom: 20px; | |
| } | |
| import { NgModule } from '@angular/core'; | |
| import { BrowserModule } from '@angular/platform-browser'; | |
| import { FormsModule } from '@angular/forms'; // Required for ngModel | |
| import { HttpClientModule } from '@angular/common/http'; // Required for API calls | |
| import { AppComponent } from './app.component'; | |
| import { TimelineComponent } from './timeline/timeline.component'; | |
| @NgModule({ | |
| declarations: [ | |
| AppComponent, | |
| TimelineComponent | |
| ], | |
| imports: [ | |
| BrowserModule, | |
| FormsModule, | |
| HttpClientModule | |
| ], | |
| providers: [], | |
| bootstrap: [AppComponent] | |
| }) | |
| export class AppModule { } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment