dev.pydantic:opamp-client is this repository's OpAMP client for Java, in
java/opamp/. It runs the shared core with Chicory, the
module compiled to JVM classes at build time: plain Java, nothing native
(how-it-works.md). It speaks plain HTTP or WebSocket, with TLS and through
HTTP or SOCKS5 proxies. Its only dependencies are Chicory and protobuf-javalite. It needs Java 17
or later. Its README has the full reference.
<dependency>
<groupId>dev.pydantic</groupId>
<artifactId>opamp-client</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>The artifact is not on Maven Central yet (sdk-registries.md). Until it is, install it into your local Maven repository from a checkout, and depend on it as above:
cd java/opamp
mvn install -DskipTestsGradle finds it there with mavenLocal() in its repositories.
import dev.pydantic.opamp.Notification;
import dev.pydantic.opamp.OpampClient;
import java.util.concurrent.CompletableFuture;
import opamp.proto.v1.Anyvalue;
import opamp.proto.v1.Opamp;
public final class Example {
public static void main(String[] args) throws Exception {
OpampClient.Config config = OpampClient.Config
.builder(System.getenv("OPAMP_ENDPOINT")) // https://.../v1/opamp, or wss://... for WebSocket
.header("Authorization", "Bearer " + System.getenv("OPAMP_TOKEN"))
.capabilities(Opamp.AgentCapabilities.AgentCapabilities_AcceptsRemoteConfig_VALUE)
.build();
Opamp.AgentDescription description = Opamp.AgentDescription.newBuilder()
.addIdentifyingAttributes(Anyvalue.KeyValue.newBuilder()
.setKey("service.name")
.setValue(Anyvalue.AnyValue.newBuilder().setStringValue("checkout")))
.build();
OpampClient.Status status = new OpampClient.Status(
description, Opamp.ComponentHealth.newBuilder().setHealthy(true).build(), null, null);
// The callback can't name `client` itself: it takes it from a future completed below.
CompletableFuture<OpampClient> started = new CompletableFuture<>();
OpampClient client = OpampClient.start(config, status, n -> {
if (n instanceof Notification.RemoteConfig rc) {
// Apply rc.config() here, then say how it went.
started.join().setRemoteConfigStatus(Opamp.RemoteConfigStatus.newBuilder()
.setLastRemoteConfigHash(rc.config().getConfigHash())
.setStatus(Opamp.RemoteConfigStatuses.RemoteConfigStatuses_APPLIED)
.build());
} else if (n instanceof Notification.Stopped s && s.error() != null) {
System.err.println("OpAMP client stopped: " + s.error());
}
});
started.complete(client);
Thread.sleep(3000); // the application's work
client.stop().join(); // sends agent_disconnect, then stops
}
}The first notification comes after start has returned, but perhaps before the caller has stored
what it returned: the callback takes the client from a future the caller completes. Give the
agent's status to start, so the first message reports it in full.
OpampClient.Config.builder(endpoint) (or builder(endpoint, instanceUid)) returns a builder;
build() checks the settings. OpampClient.Status is a record. Both are in
java/opamp/src/main/java/dev/pydantic/opamp/OpampClient.java.
| What | Option | More |
|---|---|---|
| Endpoint | builder(endpoint): http(s):// polls, ws(s):// keeps one connection open |
|
| Headers | header(name, value), sent with every request and WebSocket upgrade |
|
| TLS | tls(Tls.builder()...build()): trustedCa, includeSystemCas, clientCertificate, minVersion, maxVersion |
README |
| Proxy | proxy(url), proxyResolveLocally(true), or directConnection() for none. Without one, the environment's |
README |
| Who speaks HTTP | mode(OpampClient.Mode.CORE) (the default) or mode(OpampClient.Mode.PLATFORM) (java.net.http.HttpClient); httpClient(...) selects platform mode |
sdks.md |
| State file | stateFile(Path): kept across restarts, for the agent's user alone |
state-file.md |
| Instance UID | builder(endpoint, instanceUid) (16 bytes), or builder(endpoint) for one the core makes; requestInstanceUid(true) to have the server assign it; client.instanceUid() is the one in use |
instance-uid.md |
| Capabilities | capabilities(long) (AgentCapabilities bits). ReportsStatus, ReportsHeartbeat and the Reports* bit of each status you set are added for you |
|
| Heartbeat | heartbeatInterval(Duration) until the server offers one (30 s) |
|
| Description, health, effective config | the first three of Status at start; setDescription, setHealth, setEffectiveConfig later |
sdks.md |
| Remote config | AcceptsRemoteConfig; answer Notification.RemoteConfig with setRemoteConfigStatus |
remote-config.md |
| Connection settings | acceptConnectionSettings(true) (needs a state file), verifyTimeout(Duration) |
connection-settings.md |
| Telemetry settings | ReportsOwnTraces, ReportsOwnMetrics, ReportsOwnLogs; answer Notification.ConnectionSettings with setConnectionSettingsStatus |
telemetry-settings.md |
| Available components | Status (the fifth component), setAvailableComponents |
available-components.md |
| Packages | acceptPackages(true) (needs a state file), maxPackageBytes, downloadTimeout; answer with packageInstalled, packageRemoved |
packages.md |
| Custom messages | Status (the sixth component), setCustomCapabilities, sendCustomMessage |
custom-messages.md |
| Limits | requestTimeout, connectTimeout, stopTimeout, maxSendBytes, maxReceiveBytes |
how-it-works.md |
Certificates and keys are read when given, and the proxy URL and the mode by build(), so
mistakes fail there.
In platform mode the JDK's HttpClient carries everything, and two of its limits apply:
- HTTP proxies only.
HttpClienthas no SOCKS and nohttps://proxies, so the client refuses those URLs. Without a proxy, platform mode usesProxySelector.getDefault()(http.proxyHostand the other proxy properties), not the environment. - The JDK sends no Basic proxy credentials through a CONNECT tunnel (
https://,ws://,wss://) unless the JVM runs with-Djdk.http.auth.tunneling.disabledSchemes=. It is the JVM's setting, so the client leaves it to the application, and logs a warning at start.
Core mode, the default, has neither limit: the core speaks to the proxy itself.
The callback gets a Notification, a sealed interface of records.
| Record | Components | Answer with |
|---|---|---|
Connected, ConnectFailed |
status, detail |
|
RemoteConfig |
config |
setRemoteConfigStatus |
ConnectionSettings |
settings |
setConnectionSettingsStatus |
InstallPackage, RemovePackage |
name, pkg, path |
packageInstalled, packageRemoved |
CustomMessage, ServerCustomCapabilities, CustomMessageNotSent |
message, capabilities, reason |
|
InstanceUidChanged, HeartbeatInterval, Command |
instanceUid, interval, command |
|
ErrorResponse, BadMessage, StatusTooLarge, SaveStalled |
type, message, detail, field, bytes, stalled, waited |
|
Stopped |
error: why it stopped on its own, or null after stop() |
- The client runs on a daemon thread of its own,
opamp-client, so it doesn't keep the JVM running. The callback runs there, one call at a time, never beforestartreturns and never afterStopped. Keep it quick: the client waits for it. - Every method may be called from the callback and from any thread. None of them blocks.
stop()sendsagent_disconnectif a message can still be sent, and returns aCompletableFuture<Void>that completes when the core says it has stopped, withinstopTimeout(5 s by default). Nothing the client started is running then; a save already writing finishes whole.- From the callback, ask for the stop without waiting for it:
client.stop(), neverclient.stop().join(). - A callback that throws (any
Throwable) stops the client, without the goodbye.Stoppedcarries the error, and the futurestop()returns completes exceptionally with it. AVirtualMachineErroris rethrown on the client's thread after the client has ended.
sdk-contract.md has the contract every client here keeps.
scripts/fetch_core.py fetch # the core's test files and the adversary module
cd java/opamp
mvn package # compile and testmvn -Pfray test -Dfray.only=true explores thread schedules with Fray; the
README says what it needs. The conformance suite runs this
client in both modes: python3 conformance/runner/run.py --client java-core-platform,java-core
(conformance/README.md).
To put an application under a server without writing code, use the extension for the
OpenTelemetry Java agent: javaagent.md. To wire this client to an SDK by hand, see
sdks.md and examples/java.
dev.pydantic.opamp.sdk.SdkSettings runs the shared SDK settings layer
(sdk-settings.md).