- Introduction
- Installation & Setup
- Quick Start
- Core Concepts
- Basic Usage
- Advanced Features
- Configuration Options
- Best Practices
- Troubleshooting
- API Reference
- Examples
- Technical System Overview for developers
The VFL Client Java Framework is a Java implementation of Visual Flow Logger (VFL) that enables structured, hierarchical logging in your Java applications. Unlike traditional flat logging systems, VFL creates a tree-like structure that shows how your application executes, making debugging, monitoring, and performance analysis easier.
- Minimal non breaking Refactoring: Add
@SubBlock
annotations to existing methods - Hierarchical Logging: View your application flow as a tree structure
- Distributed Tracing: Track operations across multiple services
- Asynchronous Support: Handle parallel operations and background tasks
- Event-Driven Patterns: Support for pub/sub and event-driven architectures
- Thread Safety: Built for concurrent applications
VFL integrates into existing codebases with minimal changes:
// Your existing method
public void processPayment(String orderId) {
validatePayment(orderId);
chargeCard(orderId);
}
// Add VFL tracing - minimal changes required
@SubBlock(blockName = "Process Payment {0}")
public void processPayment(String orderId) {
validatePayment(orderId); // Same code
chargeCard(orderId); // Same code
}
- Java 11 or higher
- Maven or Gradle build system
- VFL Hub server running (see VFL Hub Setup Guide)
<dependency>
<groupId>dev.kuku.vfl</groupId>
<artifactId>vfl-client-java</artifactId>
<version>1.0.0</version>
</dependency>
implementation 'dev.kuku.vfl:vfl-client-java:1.0.0'
import dev.kuku.vfl.impl.annotation.*;
import dev.kuku.vfl.core.buffer.AsyncBuffer;
public class Application {
public static void main(String[] args) {
// Initialize VFL
VFLFlushHandler flushHandler = new VFLHubFlushHandler(
URI.create("http://localhost:8080")
);
VFLBuffer buffer = new AsyncBuffer(100, 3000, 1000, flushHandler,
Executors.newVirtualThreadPerTaskExecutor(),
Executors.newSingleThreadScheduledExecutor());
VFLInitializer.initialize(new VFLAnnotationConfig(false, buffer));
// Your application code
new MyService().processOrder("ORD-12345");
}
}
public class MyService {
@SubBlock(blockName = "Process Order {0}")
public void processOrder(String orderId) {
Log.Info("Processing order {}", orderId);
validateOrder(orderId);
chargePayment(orderId);
}
@SubBlock(blockName = "Validate Order {0}")
private void validateOrder(String orderId) {
Log.Info("Validating order");
}
public void runExample() {
VFLStarter.StartRootBlock("Order Processing", () -> {
processOrder("ORD-12345");
});
}
}
Open your VFL Hub dashboard at http://localhost:8080
to view the structured flow visualization.
Blocks are containers that represent a scope of execution:
- Root Block: The top-level container for your entire operation
- Sub-Block: Child blocks created by
@SubBlock
annotations - Event Listener Block: Blocks that handle asynchronous events
- Continuation Block: Blocks that continue a trace from another service
VFL automatically creates different types of blocks based on the context:
- Primary Sub-Block: Main execution path blocks created by
@SubBlock
- Secondary Sub-Block (No Join): Async operations that don't need to be waited for
- Secondary Sub-Block (Join): Async operations that will be joined/awaited
- Event Publisher: Blocks that publish events for other services to consume
- Event Listener: Blocks that handle published events
Logs are individual events that happen within blocks:
VFL supports three main types of logs:
- Message: Regular informational logs (
Log.Info()
) - Warning: Warning messages that indicate potential issues (
Log.Warn()
) - Error: Error messages for problems that need attention (
Log.Error()
)
When you add @SubBlock
annotations, VFL automatically creates this structure:
Root Block: "Process User Registration"
├── Log: "Starting registration process"
├── Sub-Block: "Validate Email" ← Created by @SubBlock
│ └── Log: "Email validation passed"
├── Sub-Block: "Create User Account" ← Created by @SubBlock
│ └── Log: "User account created"
└── Log: "Registration completed"
// Simple root block
public void handleRequest() {
VFLStarter.StartRootBlock("Handle Request", () -> {
myService.processRequest();
});
}
// Root block with return value
public String processOrder() {
return VFLStarter.StartRootBlock("Process Order", () -> {
return orderService.process();
});
}
@SubBlock(blockName = "Validate User {0}")
public boolean validateUser(String userId) {
Log.Info("Validating user {}", userId);
return checkPermissions(userId);
}
// Simple logging
Log.Info("Processing started");
Log.Warn("Low memory detected");
Log.Error("Database connection failed");
// With parameters
Log.Info("Processing user {} with status {}", userId, status);
public void processAsync() {
VFLStarter.StartRootBlock("Async Processing", () -> {
CompletableFuture<String> future =
VFLFutures.supplyAsync(() -> doWork());
String result = future.join();
Log.Info("Result: {}", result);
});
}
@SubBlock
private String doWork() {
Log.Info("Working...");
return "Done";
}
public void orderProcessing() {
VFLStarter.StartRootBlock("Order Processing", () -> {
// Publish an event
EventPublisherBlock event = Log.Publish("Order Created",
"Order {} created", orderId);
// Handle the event
handleOrderEvent(event);
});
}
private void handleOrderEvent(EventPublisherBlock event) {
VFLStarter.StartEventListener(event, "Order Handler", () -> {
Log.Info("Handling order event");
});
}
// Service A
public void serviceA() {
VFLStarter.StartRootBlock("Service A", () -> {
Log.CreateContinuationBlock("Call Service B", block -> {
callServiceB(serialize(block));
});
});
}
// Service B
public void serviceB(String serializedBlock) {
Block block = deserialize(serializedBlock);
VFLStarter.ContinueFromBlock(block, () -> {
Log.Info("Continuing in Service B");
});
}
// Synchronous - flushes immediately
VFLBuffer buffer = new SynchronousBuffer(100, flushHandler);
// Asynchronous - flushes in background
VFLBuffer buffer = new AsyncBuffer(100, 3000, 1000, flushHandler,
executor, scheduledExecutor);
// No-Op - discards logs (testing)
VFLBuffer buffer = new NoOpsBuffer();
// VFL Hub (recommended for production)
VFLFlushHandler handler = new VFLHubFlushHandler(
URI.create("http://localhost:8080"));
// File handler (testing only)
VFLFlushHandler handler = new NestedJsonFlushHandler("output.json");
// Correct
public void handleRequest() {
VFLStarter.StartRootBlock("Handle Request", () -> {
processRequest(); // @SubBlock methods work here
});
}
// Incorrect - no parent context
@SubBlock
public void processRequest() {
// This will show a warning
}
// Good
@SubBlock(blockName = "Validate Credit Card for Order {0}")
// Better
@SubBlock(blockName = "Process Payment - Order {0}, Amount ${1}")
// Avoid
@SubBlock(blockName = "Method execution")
// Correct - maintains trace context
CompletableFuture<String> future = VFLFutures.supplyAsync(() -> work());
// Incorrect - loses context
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> work());
Ensure @SubBlock
methods are called within a root block.
- Check VFL Hub is running
- Verify network connectivity
- Check buffer flush settings
Reduce buffer size or flush interval.
Method | Description |
---|---|
StartRootBlock(String, Runnable) |
Start new root block |
StartRootBlock(String, Supplier<T>) |
Start root block with return value |
ContinueFromBlock(Block, Runnable) |
Continue from existing block |
StartEventListener(EventPublisherBlock, String, Runnable) |
Start event listener |
Method | Description |
---|---|
Info(String, Object...) |
Log info message |
Warn(String, Object...) |
Log warning |
Error(String, Object...) |
Log error |
Publish(String, String, Object...) |
Publish event |
Attribute | Description |
---|---|
blockName |
Block display name with {0}, {1} placeholders |
startMessage |
Optional message logged on entry |
endMessage |
Optional message logged on exit |
public class OrderService {
public void processOrder(String orderId) {
VFLStarter.StartRootBlock("Process Order", () -> {
validateOrder(orderId);
chargePayment(orderId);
createRecord(orderId);
});
}
@SubBlock(blockName = "Validate Order {0}")
private void validateOrder(String orderId) {
Log.Info("Validating order {}", orderId);
}
@SubBlock(blockName = "Charge Payment {0}")
private void chargePayment(String orderId) {
Log.Info("Processing payment for {}", orderId);
}
@SubBlock(blockName = "Create Record {0}")
private void createRecord(String orderId) {
Log.Info("Creating order record for {}", orderId);
}
}
public class AsyncService {
public void processAsync(String id) {
VFLStarter.StartRootBlock("Async Processing", () -> {
CompletableFuture<String> task1 =
VFLFutures.supplyAsync(() -> doTask1(id));
CompletableFuture<String> task2 =
VFLFutures.supplyAsync(() -> doTask2(id));
String result1 = task1.join();
String result2 = task2.join();
Log.Info("Results: {} | {}", result1, result2);
});
}
@SubBlock
private String doTask1(String id) {
Log.Info("Executing task 1 for {}", id);
return "Task1 complete";
}
@SubBlock
private String doTask2(String id) {
Log.Info("Executing task 2 for {}", id);
return "Task2 complete";
}
}
public class EventService {
public void processWithEvents(String orderId) {
VFLStarter.StartRootBlock("Event-Driven Processing", () -> {
Log.Info("Processing order {}", orderId);
// Publish event
EventPublisherBlock event = Log.Publish("Order Processed",
"Order {} completed", orderId);
// Handle event
handleOrderComplete(event, orderId);
});
}
private void handleOrderComplete(EventPublisherBlock event, String orderId) {
VFLStarter.StartEventListener(event, "Order Complete Handler", () -> {
sendEmail(orderId);
updateInventory(orderId);
});
}
@SubBlock
private void sendEmail(String orderId) {
Log.Info("Sending email for order {}", orderId);
}
@SubBlock
private void updateInventory(String orderId) {
Log.Info("Updating inventory for order {}", orderId);
}
}
Technical System Overview for developers
For more information, visit the GitHub repository.