Skip to content
Open
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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ Drop-in observability for Spring AI and LangChain4j agents. Mirrors the Python `
<dependency>
<groupId>dev.jamjet</groupId>
<artifactId>jamjet-cloud-spring-boot-starter</artifactId>
<version>0.2.0</version>
<version>0.3.1</version>
</dependency>
```

Expand All @@ -141,13 +141,15 @@ jamjet:

That's it. Every Spring AI `ChatClient` / `ChatModel` call and every LangChain4j `ChatLanguageModel` call is captured automatically.

See examples/jamjet-cloud-sdk-demo for a runnable end-to-end example.

Comment on lines +144 to +145

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the demo pointer a clickable Markdown link.

At Line 144, use a link ([examples/jamjet-cloud-sdk-demo](examples/jamjet-cloud-sdk-demo/)) so readers can navigate directly from rendered docs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 144 - 145, Replace the plain text reference "See
examples/jamjet-cloud-sdk-demo for a runnable end-to-end example." with a
Markdown link so it becomes clickable in rendered docs; update the line that
contains "See examples/jamjet-cloud-sdk-demo" to use the link text
[examples/jamjet-cloud-sdk-demo](examples/jamjet-cloud-sdk-demo/) (including the
trailing slash) so readers can navigate directly to the demo folder.

### Plain Java (LangChain4j without Spring)

```xml
<dependency>
<groupId>dev.jamjet</groupId>
<artifactId>jamjet-cloud-sdk</artifactId>
<version>0.2.0</version>
<version>0.3.1</version>
</dependency>
```

Expand Down
46 changes: 46 additions & 0 deletions examples/jamjet-cloud-sdk-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# JamJet Cloud SDK Demo (Java)

A Java app using the JamJet Cloud SDK end to end: it emits **spans** (observability:
model, tokens, cost) to JamJet Cloud and **AgentBoundary Action Receipts** (audit) for
tool calls. Two flavours:

- **Spring Boot** (`CloudDemoApplication`): `jamjet-cloud-spring-boot-starter` auto-wires
span observability; an `ActionReceiptAdvisor` + a `FileActionReceiptEmitter` add receipts
for `@Tool` calls. Spans are zero-config; receipts are one advisor + emitter bean.
- **Plain Java** (`PlainJavaCloudDemo`): the raw `JamjetCloud` / `Span` / `ActionReceipt` API.

## Build & test (hermetic, no secrets)

From the repo root:
```bash
mvn -pl examples/jamjet-cloud-sdk-demo -am test
```
Tests stub JamJet Cloud with WireMock and use a deterministic model, so they need no API keys.

## Live mode (see it in the dashboard)

```bash
export JJ_API_KEY=... # your JamJet Cloud key
export OPENAI_API_KEY=... # for the Spring demo's real model

# Spring demo
mvn -pl examples/jamjet-cloud-sdk-demo spring-boot:run

# Plain-Java demo
mvn -pl examples/jamjet-cloud-sdk-demo -am package -DskipTests
java -cp examples/jamjet-cloud-sdk-demo/target/classes:... dev.jamjet.example.cloud.PlainJavaCloudDemo
```
Comment on lines +30 to +32

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Plain-Java run command is not copy-paste runnable.

At Line 31, -cp ... contains an unresolved placeholder, so users can’t run the example as written. Provide a concrete command (e.g., via exec-maven-plugin or documented classpath generation) to keep the live-mode path executable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/jamjet-cloud-sdk-demo/README.md` around lines 30 - 32, Replace the
non-runnable example Java launch line in README.md for PlainJavaCloudDemo with a
concrete, copy-pasteable run approach: either show running the demo via Maven’s
exec plugin (calling main class dev.jamjet.example.cloud.PlainJavaCloudDemo) or
document how to generate an explicit classpath (e.g., build target/classes plus
resolved dependency JARs) and use that concrete classpath in the java -cp
invocation; update the README’s example so the placeholder “...” is removed and
the user can run the demo directly.

Then: the **span** appears in the JamJet Cloud dashboard for project `cloud-sdk-demo`, and
the **receipt** is written to `~/.jamjet/audit/cloud-sdk-demo.jsonl`.

## What is and isn't shown

Spans are the cloud-visible artifact today. Receipts are emitted locally (file/log) -- the
SDK's cloud-posting receipt emitter is planned, not yet shipped. The Java Cloud SDK does
**not** yet include local policy enforcement, PII redaction, an approval UX, or cache
injection; those exist in the TypeScript/Python SDKs and the `jamjet-policy` tooling.

## Build note

This example is a reactor module so it builds against the in-repo SDK. Once
`jamjet-cloud-sdk` is published to Maven Central it can be split out as a standalone project.
94 changes: 94 additions & 0 deletions examples/jamjet-cloud-sdk-demo/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?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>

<parent>
<groupId>dev.jamjet</groupId>
<artifactId>jamjet-runtime-java-parent</artifactId>
<version>0.3.1</version>
<relativePath>../../pom.xml</relativePath>
</parent>

<artifactId>jamjet-cloud-sdk-demo</artifactId>
<packaging>jar</packaging>
<name>JamJet Cloud SDK Demo</name>
<description>End-to-end example: Java app using the JamJet Cloud SDK (spans + AgentBoundary receipts).</description>

<properties>
<spring-boot.version>3.3.5</spring-boot.version>
<spring-ai.version>1.0.0</spring-ai.version>
<wiremock.version>3.9.1</wiremock.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>dev.jamjet</groupId>
<artifactId>jamjet-cloud-spring-boot-starter</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<!-- langchain4j-core is optional in the starter but the JamjetCloudAutoConfiguration
return type references JamjetChatModelListener which imports it; include here so
Spring's class introspection can resolve it at context startup. -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-core</artifactId>
<version>0.36.2</version>
</dependency>
Comment on lines +62 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Demo module langchain4j-core declaration =="
rg -n -C2 '<artifactId>langchain4j-core</artifactId>|<version>0\.36\.2</version>' examples/jamjet-cloud-sdk-demo/pom.xml

echo
echo "== Starter module references to langchain4j (if any) =="
fd -i 'pom.xml' | xargs rg -n -C2 'langchain4j|JamjetChatModelListener|ConditionalOnClass' || true

echo
echo "== Parent/version properties that might manage langchain4j =="
rg -n -C2 'langchain4j|spring-ai.version|spring-boot.version' pom.xml examples/jamjet-cloud-sdk-demo/pom.xml

Repository: jamjet-labs/jamjet-runtime-java

Length of output: 6368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== examples/jamjet-cloud-sdk-demo/pom.xml (around langchain4j-core) =="
nl -ba examples/jamjet-cloud-sdk-demo/pom.xml | sed -n '1,140p'

echo
echo "== jamjet-cloud-spring-boot-starter/pom.xml (around langchain4j-core) =="
nl -ba jamjet-cloud-spring-boot-starter/pom.xml | sed -n '1,140p'

echo
echo "== repository root pom.xml (dependencyManagement relevant bits) =="
nl -ba pom.xml | sed -n '1,220p'

echo
echo "== any dependencyManagement for langchain4j-core in repo =="
rg -n --glob 'pom.xml' 'dependencyManagement' pom.xml examples jamjet-cloud-* | head -n 50

echo
echo "== any centralized property for langchain4j-core version =="
rg -n --glob 'pom.xml' 'langchain4j.*version' pom.xml examples jamjet-cloud-* | head -n 50

Repository: jamjet-labs/jamjet-runtime-java

Length of output: 207


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== examples/jamjet-cloud-sdk-demo/pom.xml (around langchain4j-core) =="
cat -n examples/jamjet-cloud-sdk-demo/pom.xml | sed -n '1,120p'
echo
echo "== jamjet-cloud-spring-boot-starter/pom.xml (around langchain4j-core) =="
cat -n jamjet-cloud-spring-boot-starter/pom.xml | sed -n '1,140p'
echo
echo "== repository root pom.xml (dependencyManagement relevant bits) =="
cat -n pom.xml | sed -n '1,240p'

echo
echo "== dependencyManagement blocks mentioning langchain4j-core =="
rg -n --glob 'pom.xml' 'dependencyManagement' pom.xml examples jamjet-cloud-* || true

echo
echo "== centralized properties for langchain4j version (if any) =="
rg -n --glob 'pom.xml' 'langchain4j.*version' pom.xml examples jamjet-cloud-* || true

echo
echo "== any direct langchain4j-core pinning =="
rg -n --glob 'pom.xml' '<artifactId>langchain4j-core</artifactId>' pom.xml examples jamjet-cloud-* | head -n 50

Repository: jamjet-labs/jamjet-runtime-java

Length of output: 19231


Avoid duplicating/hard-pinning langchain4j-core in examples/jamjet-cloud-sdk-demo/pom.xml.

langchain4j-core is explicitly pinned to 0.36.2 at line 64, and the repo parent doesn’t centrally manage a langchain4j version. Since jamjet-cloud-spring-boot-starter also pins langchain4j-core 0.36.2 as <optional>true</optional>, the demo must keep an explicit dependency—but the version should be centralized/shared (e.g., parent property or shared dependencyManagement) to avoid updating both poms in lockstep later.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/jamjet-cloud-sdk-demo/pom.xml` around lines 62 - 65, The pom
currently hard-pins langchain4j-core version "0.36.2" in the
examples/jamjet-cloud-sdk-demo POM; remove the literal <version>0.36.2</version>
from the dependency for artifactId langchain4j-core and instead reference a
centralized property (e.g. use <version>${langchain4j.version}</version>) or
rely on a parent/shared dependencyManagement entry; ensure the examples POM
still declares the dependency for langchain4j-core (since
jamjet-cloud-spring-boot-starter marks it optional) but uses the centralized
property or dependencyManagement entry so the version is managed in one place.


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>${wiremock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.jamjet.example.cloud;

import dev.jamjet.cloud.spring.ActionReceiptAdvisor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ChatConfig {

@Bean
public ChatClient chatClient(ChatModel chatModel, ActionReceiptAdvisor advisor, LookupTool lookupTool) {
return ChatClient.builder(chatModel)
.defaultAdvisors(advisor)
.defaultTools(lookupTool)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package dev.jamjet.example.cloud;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;

@SpringBootApplication
public class CloudDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CloudDemoApplication.class, args);
}

/** Live run only (not under test): make one governed call. */
@Bean
@Profile("!test")
CommandLineRunner demo(ChatClient chatClient) {
return args -> {
String reply = chatClient.prompt()
.user("What is the status of order A-100? Use the orderStatus tool.")
.call()
.content();
System.out.println("Agent reply: " + reply);
System.out.println("A span was sent to JamJet Cloud; a receipt was written to ~/.jamjet/audit/cloud-sdk-demo.jsonl");
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package dev.jamjet.example.cloud;

import com.fasterxml.jackson.databind.ObjectMapper;
import dev.jamjet.cloud.agentboundary.ActionReceipt;
import dev.jamjet.cloud.agentboundary.ActionReceiptEmitter;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

/**
* Appends each Action Receipt as one JSON line to a file (JSONL), mirroring the
* {@code ~/.jamjet/audit/} pattern used by the TypeScript/Python SDKs. The Java
* Cloud SDK does not yet ship a cloud-posting emitter, so this makes the audit
* artifact tangible and inspectable.
*/
public final class FileActionReceiptEmitter implements ActionReceiptEmitter {

private static final ObjectMapper MAPPER = new ObjectMapper().findAndRegisterModules();

private final Path file;
private final Object lock = new Object();

public FileActionReceiptEmitter(Path file) {
this.file = file;
}

@Override
public void emit(ActionReceipt receipt) {
try {
String line = MAPPER.writeValueAsString(receipt) + System.lineSeparator();
synchronized (lock) {
if (file.getParent() != null) {
Files.createDirectories(file.getParent());
}
Files.writeString(file, line, StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
}
} catch (IOException e) {
throw new UncheckedIOException("failed to write receipt to " + file, e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dev.jamjet.example.cloud;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;

@Component
public class LookupTool {
@Tool(description = "Look up the shipping status of an order by its id.")
public String orderStatus(@ToolParam(description = "The order id, e.g. A-100") String orderId) {
return "Order " + orderId + " is shipped.";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package dev.jamjet.example.cloud;

import dev.jamjet.cloud.JamjetCloud;
import dev.jamjet.cloud.JamjetCloudConfig;
import dev.jamjet.cloud.Span;
import dev.jamjet.cloud.agentboundary.*;

import java.nio.file.Path;
import java.time.Instant;
import java.util.UUID;

/**
* Plain-Java (no Spring) demo of the raw JamJet Cloud SDK API:
* open a span (observability) and emit an AgentBoundary receipt (audit).
*/
public final class PlainJavaCloudDemo {

private PlainJavaCloudDemo() {}

/** Testable entry point: configure the SDK, emit a span, build + emit a receipt. */
public static void run(JamjetCloudConfig config, ActionReceiptEmitter emitter) {
JamjetCloud.configure(config);

// 1) Observability: a span for a (simulated) model call.
Span span = JamjetCloud.newSpan("model", "demo-call");
span.model("gpt-4o-mini").inputTokens(120).outputTokens(35).costUsd(0.0004);
span.finish();

// 2) Audit: an AgentBoundary Action Receipt for a (simulated) tool call.
String argsJson = "{\"orderId\":\"A-100\"}";
String argsHash = ReceiptHashes.computeArgumentsHash(argsJson);
String receiptId = UUID.randomUUID().toString();
String now = Instant.now().toString();
Actor actor = new Actor(ActorType.AGENT, "cloud-sdk-demo", null);
Agent agent = new Agent("jamjet-runtime-java", "0.3.1", "gpt-4o-mini", null);
Tool tool = new Tool("orderStatus", null, "orderStatus");
Target target = new Target("cloud-sdk-demo", Environment.DEV, "A-100");
Policy policy = new Policy("default.allow", "1", PolicyDecision.ALLOW);
Execution execution = new Execution(ExecutionStatus.SUCCESS, now, null, "shipped");
String receiptHash = ReceiptHashes.computeReceiptHash(
ActionReceipt.CURRENT_VERSION, receiptId, now, actor, agent, tool, target,
argsHash, policy, null, execution);
ActionReceipt receipt = new ActionReceipt(ActionReceipt.CURRENT_VERSION, receiptId, now,
actor, agent, tool, target, argsHash, policy, null, execution, receiptHash);
java.util.List<String> errors = new ActionReceiptValidator().validate(receipt);
if (!errors.isEmpty()) {
throw new IllegalStateException("receipt failed schema validation: " + errors);
}
emitter.emit(receipt);
}

public static void main(String[] args) {
String apiKey = System.getenv().getOrDefault("JJ_API_KEY", "");
String apiUrl = System.getenv().getOrDefault("JAMJET_API_URL", "https://api.jamjet.dev");
Path receipts = Path.of(System.getProperty("user.home"), ".jamjet", "audit", "cloud-sdk-demo.jsonl");
if (apiKey.isBlank()) {
System.err.println("Set JJ_API_KEY to send the span to JamJet Cloud (api.jamjet.dev).");
}
JamjetCloudConfig cfg = JamjetCloudConfig.builder()
.apiKey(apiKey).apiUrl(apiUrl).project("cloud-sdk-demo").build();
run(cfg, new FileActionReceiptEmitter(receipts));
System.out.println("Span sent to " + apiUrl + "; receipt written to " + receipts);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package dev.jamjet.example.cloud;

import dev.jamjet.cloud.agentboundary.ActionReceiptEmitter;
import dev.jamjet.cloud.spring.ActionReceiptAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;

import java.nio.file.Path;

@Configuration
public class ReceiptConfig {

@Bean
public ActionReceiptEmitter actionReceiptEmitter() {
Path file = Path.of(System.getProperty("user.home"), ".jamjet", "audit", "cloud-sdk-demo.jsonl");
return new FileActionReceiptEmitter(file);
Comment on lines +16 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make receipt file path configurable instead of hardcoding ${user.home}.

Line 17 persists receipts to a fixed user-home path, which can leak unredacted demo/test data and breaks hermetic test isolation. Prefer a property-backed path with a safe default, then override it in application-test.yml.

Proposed diff
+import org.springframework.beans.factory.annotation.Value;
...
     `@Bean`
-    public ActionReceiptEmitter actionReceiptEmitter() {
-        Path file = Path.of(System.getProperty("user.home"), ".jamjet", "audit", "cloud-sdk-demo.jsonl");
-        return new FileActionReceiptEmitter(file);
+    public ActionReceiptEmitter actionReceiptEmitter(
+            `@Value`("${jamjet.cloud.receipt-file:${user.home}/.jamjet/audit/cloud-sdk-demo.jsonl}") String receiptFile) {
+        return new FileActionReceiptEmitter(Path.of(receiptFile));
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public ActionReceiptEmitter actionReceiptEmitter() {
Path file = Path.of(System.getProperty("user.home"), ".jamjet", "audit", "cloud-sdk-demo.jsonl");
return new FileActionReceiptEmitter(file);
import org.springframework.beans.factory.annotation.Value;
...
`@Bean`
public ActionReceiptEmitter actionReceiptEmitter(
`@Value`("${jamjet.cloud.receipt-file:${user.home}/.jamjet/audit/cloud-sdk-demo.jsonl}") String receiptFile) {
return new FileActionReceiptEmitter(Path.of(receiptFile));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@examples/jamjet-cloud-sdk-demo/src/main/java/dev/jamjet/example/cloud/ReceiptConfig.java`
around lines 16 - 18, The actionReceiptEmitter() currently hardcodes
Path.of(System.getProperty("user.home"), ".jamjet", "audit",
"cloud-sdk-demo.jsonl"), which breaks test isolation and leaks demo data; change
it to read a configurable property (e.g., receipt.file.path or similar) with a
safe default that matches the current home-based path, and pass that Path into
new FileActionReceiptEmitter(...) so tests can override it via
application-test.yml; locate and update the actionReceiptEmitter() factory
method (and any config class) to inject the property (using `@Value` or
ConfigurationProperties/Environment) and ensure the default is preserved if the
property is not supplied.

}

@Bean
public ActionReceiptAdvisor actionReceiptAdvisor(Environment env, ActionReceiptEmitter emitter) {
return new ActionReceiptAdvisor(env, emitter, Ordered.LOWEST_PRECEDENCE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** Runnable example: a Java app using the JamJet Cloud SDK end to end. */
package dev.jamjet.example.cloud;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
spring:
ai:
openai:
api-key: test-key
base-url: http://localhost:0
jamjet:
cloud:
api-key: test-key
api-url: http://localhost:0
batch:
size: 1
interval-ms: 100
Loading
Loading