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",
getDescription().getVersion())
.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();The third argument to builder is the program's own version, and it is
required: a blank one, or one over 255 characters, throws
IllegalArgumentException. Every event the client sends — startup,
command, anything else — carries it as the tag version, so every event
can be tied to a release, not just startup. An event that passes its own
version tag keeps it. There is no need to tag startup by hand any more.
Before 0.4.0, builder took two arguments and only events tagged by hand
carried a version. Upgrading is one argument: in a Bukkit plugin,
getDescription().getVersion().
| 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, and the program's version is the event's
own: a server-wide
versionnever overwrites it. - 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.4.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","version":"1.4.0"}}value is omitted when not given; tags always holds at least version. 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.