One call to report that a program was used.
A dependency-free Java 8 client for a trace
server — the central place a fleet of programs reports usage events to. The
whole library is one file, TraceClient.java, and the integration on the
program side is meant to stay one call.
TraceClient trace = TraceClient.builder("https://trace.danielstephenson.dev", "MyPlugin")
.key(config.getString("usage-reporting.key"))
.enabled(config.getBoolean("usage-reporting.enabled", true))
.serverWideConfig(getDataFolder().getParentFile()) // plugins/ -- Bukkit plugins only
.logger(getLogger())
.build();
// Say so, every startup, on the program's own logger.
if (trace.isEnabled()) {
getLogger().info("Usage reporting is on: MyPlugin sends its name, version and command names to "
+ "https://trace.danielstephenson.dev - nothing about players or the server. "
+ "Turn it off with usage-reporting.enabled: false in this plugin's config.yml, "
+ "or for every plugin with enabled: false in plugins/trace/config.yml. "
+ "Details: https://github.com/Stephenson-Software/trace#usage-reporting");
} else {
getLogger().info("Usage reporting is off (" + trace.disabledReason() + ").");
}
trace.report("startup");
trace.report("command", 1.0, Collections.singletonMap("name", "home"));
// on shutdown
trace.close();| Property | Meaning |
|---|---|
| Returns immediately | The HTTP call runs on one daemon thread the client owns. A Spigot plugin can report from the server thread and no tick waits on the network. |
| Never throws | A server that is down, slow, or rejecting the key is a dropped report, not an exception in your program. Drops are logged at FINE if you gave a logger, otherwise not at all. |
| Bounded | At most 256 reports wait to be sent; past that, new ones are dropped. A trace server that is unreachable for a week costs a few kilobytes, not your heap. |
close() drains |
Reports already queued get up to the client timeout (5 s total) to be sent before the thread stops, so a CLI that reports and exits at once does not lose its event. Still bounded: an unreachable server delays exit by at most the timeout. |
Reporting is opt-out, and the person running the program always has the
last word. build() checks these in order; the first match wins and is what
disabledReason() returns, verbatim, so the program can log it:
| Switch | disabledReason() |
|---|---|
Environment: TRACE_USAGE_REPORTING=off (or false, 0, no) or DO_NOT_TRACK=1 (or true, yes), case-insensitive. Always checked. |
environment |
Server-wide, when serverWideConfig(pluginsDirectory) was given: enabled: false in plugins/trace/config.yml. build() creates the file with enabled: true (and a commented-out tags: example) if it is missing and never rewrites it afterwards; it is read with a line regex, no YAML library. An IO failure is logged at FINE and counts as enabled. |
server-wide config: plugins/trace/config.yml |
The program's own setting: enabled(false). |
config.yml |
| No key, or a blank one. | no key |
disabledReason() is null when the client is enabled. A disabled client does
nothing and costs nothing. A program that runs on other people's machines
should expose its own switch in its configuration and print, on every
startup, whether reporting is on and how to turn it off — see the example
above and the usage reporting
page for the wording the fleet uses.
The same plugins/trace/config.yml can carry a tags: block. Every event
every plugin on that server reports gets these tags added — the way a test or
CI server marks itself so its events are left out of real-installation
figures (the trace server's public numbers exclude ci, service and page):
enabled: true
tags:
ci: "true"tags:starts at column 0 and is followed by indentedkey: valuelines. Values may be double-quoted, single-quoted or bare; blank lines and#comments inside the block are skipped. The block ends at the next line that is not indented, or at the end of the file.- An event's own tag always wins: a server-wide
versionnever overwrites theversiona plugin sends. - Entries the trace server would reject are dropped one by one, never the
whole report: keys must match
[A-Za-z0-9][A-Za-z0-9_.-]*, keys and values are at most 255 characters, and server-wide tags stop being added once an event carries 32 tags in total. Anything the line reader does not understand (flow maps, lists, block scalars, a quote never closed) is dropped the same way; a malformed file never throws and never turns reporting off. - The tags are read once, in
build(), together withenabled:.enabled: falsestill wins — a disabled client sends nothing, tags or not. - A file created by
build()has the example above commented out, so nothing is added until the operator uncomments it.
Copy the file. src/main/java/software/stephenson/trace/TraceClient.java
has no dependencies and compiles on Java 8. Drop it into your source tree,
keep the header so it can be found again, and you are done — the same way
plugins already vendor bStats' Metrics.java.
Or depend on it via JitPack:
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependency>
<groupId>com.github.Stephenson-Software</groupId>
<artifactId>trace-client-java</artifactId>
<version>0.3.0</version>
</dependency>Shade it into a plugin jar; it is one class.
POST {baseUrl}/api/metrics with Authorization: Bearer <key> and a body of
{"application":"MyPlugin","name":"command","value":1.0,"tags":{"name":"home"}}value and tags are omitted when not given. The server assigns the
timestamp. A 201 is success; anything else is logged at FINE and dropped.
A key identifies the program to the server and lets the operator revoke it; it is scoped to reporting only. Because it ships inside the program, it cannot prove anything — treat trace data as best-effort telemetry, which is what it is. Ask the trace operator for a key for your program.
mvn verify
Tests run the client against the JDK's own HttpServer on a loopback port —
no more dependencies than the client itself. CI runs them on Java 8, 17 and 21.
MIT.