The Gradle Plugin
The net.benelog.spidersense plugin puts -javaagent: on the JVM that Gradle forks for bootRun or run, so an application starts under Spider Sense with no path and no JVM argument in the build file.
plugins {
id 'org.springframework.boot' version '4.1.1'
id 'net.benelog.spidersense' version '0.1.0'
}
./gradlew bootRun
That is the whole setup. The application starts under Spider Sense, named after the project, and the UI is at http://127.0.0.1:4000.
The jar is resolved from Maven Central as net.benelog.spidersense:spider-sense:0.1.0, the plugin’s own version, through the project’s repositories.
mavenCentral() therefore has to be among them, as it is in every Spring Boot project.
The plugin itself is a plain Gradle plugin published to Maven Central with its marker, which the Gradle Plugin Portal proxies, so the default pluginManagement finds it.
The plugin applies nothing else and configures nothing it was not asked to.
It works the same with the application plugin’s run task.
A project with neither Spring Boot nor application gets the spiderSense block and the three tasks, and nothing attached.
A build that cannot take a new plugin puts the argument on the task itself, with an absolute path to the jar:
tasks.named('bootRun') {
jvmArgs '-javaagent:/absolute/path/to/spider-sense-0.1.0.jar', '-Dotel.service.name=spring-orders'
}
What applying it does
-
Creates the
spiderSenseextension, the block below. -
Creates the
spiderSenseconfiguration, resolvable and not consumable, non-transitive, with one default dependency:net.benelog.spidersense:spider-sense:<version>, whereversionis the block’s property and defaults to the plugin’s own version. A dependency added to the configuration by the build replaces the default. That is how a project on a checkout of this repository uses the jar it just built:dependencies { spiderSense project(path: ':spider-sense-agent', configuration: 'senseJar') }. -
Attaches to every
JavaExectask whose name is inattachTo(defaultbootRun,bootTestRun,run) by adding aCommandLineArgumentProviderto the task’sjvmArgumentProviders. The provider contributes-javaagent:<jar>first, then one-Dspidersense.<key>=<value>per property of the block that has a value. The jar is a declared input of the task, so a project dependency on it is built first. A task that is not inattachTo, or a run withspiderSense.enabledfalse, gets nothing: no argument, no jar resolution. Adding a provider rather than editingjvmArgsleaves the task’s ownjvmArgsalone. The arguments are computed when the task runs, so aspiderSense { }block anywhere in the build file, before or aftertasks.named('bootRun'), is seen. -
Registers the tasks
spiderSense,spiderSenseInitandspiderSenseCheckin the groupspider sense.
Nothing here touches the Gradle daemon.
A jvmArgumentProvider reaches only the forked JVM, which is the reason the plugin exists instead of JAVA_TOOL_OPTIONS.
JAVA_TOOL_OPTIONS reaches every JVM the command starts, including the daemon, which then tries to host a Spider Sense of its own on port 4000.
The block
Every property is a lazy Gradle Property.
Unset means "leave the jar’s own default", and the jar’s defaults are in Configuration.
| Property | Type | Default | Becomes |
|---|---|---|---|
|
|
|
nothing is attached when |
|
|
the plugin’s version |
the version of |
|
|
unset |
the jar to attach instead of resolving one; the project property |
|
|
|
the names of the |
|
|
|
|
|
|
unset ( |
|
|
|
unset ( |
|
|
|
unset |
|
|
|
unset ( |
|
|
|
unset ( |
|
|
|
unset ( |
|
|
|
unset ( |
|
|
|
unset ( |
|
|
|
unset |
|
|
|
unset (the jar’s default list) |
|
|
|
unset ( |
|
|
|
unset |
|
|
a nested block |
the rules of the |
Where the jar comes from, in order: the project property spiderSense.jar, then the block’s jar, then the single file of the spiderSense configuration.
More than one file in the configuration, or none, is an error naming the configuration when a task that needs the jar runs.
The block sets only spidersense. properties.
Everything the OpenTelemetry agent takes as otel. goes on the task as usual, and the two combine:
spiderSense {
port = 4001
slowQueryMs = 50
appPackages = ['com.acme.orders']
}
tasks.named('bootRun') {
jvmArgs '-Dotel.instrumentation.jdbc-datasource.enabled=true'
}
Switching it off and on
./gradlew bootRun -PspiderSense.enabled=false runs the application bare.
spiderSense { enabled = false } with -PspiderSense.enabled=true is the other way round: kept in the build, attached on request.
An enabled of false is the same as not applying the plugin, except that the block and the tasks are still there.
The tasks
spiderSense runs the jar exactly as java -jar spider-sense.jar would, so both the standalone server and the CLI are one Gradle task away in a project that never checked out this repository:
./gradlew spiderSense # the standalone collector + UI; Ctrl-C stops it
./gradlew -q spiderSense --args="findings --since=start" # the CLI; -q keeps Gradle's own lines out
./gradlew -q spiderSense --args="check --max-n-plus-one=0"
It is a JavaExec with the jar as its only class path and no dependency on compiling the project.
The block’s spidersense.* properties are passed to it as they are to the application, so a standalone started this way listens where the block says.
It also sets the environment variable SPIDERSENSE_URL to the base URL the block implies, which is collector when set and otherwise http://<host>:<port> with the defaults filled in.
A CLI command therefore asks the Spider Sense the application is sending to rather than the default port.
The exit code is the CLI’s exit code, which is what check is for.
spiderSenseInit runs init --dir=<the project directory> --jar=<the jar>.
It writes the Spider Sense block into the project’s CLAUDE.md and installs the skills into .claude/skills/, as The Agent Skill describes.
The jar path it writes is wherever Gradle resolved the jar to, a file under ~/.gradle/caches/ for a Maven Central jar, which stays valid until the version changes.
Run it again after a version bump.
Check as a build step
spiderSenseCheck runs the CLI’s check with the rules of the check { } block and fails the build when the verdict is fail:
spiderSense {
check {
since = 'start' // the default: since the application was last started
maxP95Ms = 300
maxNPlusOne = 0
maxErrors = 0
}
}
./gradlew bootRun & # or the test task forwarding to a standalone, as below
./gradlew test spiderSenseCheck
| Property | Type | Becomes |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
default |
The task is the spiderSense task with check and those arguments.
It asks the Spider Sense the block implies, through SPIDERSENSE_URL as above, and prints the check’s text rendering.
Exit code 1 fails the build with Spider Sense check failed.
Exit code 3 fails it with Spider Sense check had no request to judge, unless failOnNoRequests is false.
Exit code 2 or 4 fails it with Spider Sense check could not run (exit <n>).
The CLI’s own message reaches the build log first in every case.
With no rule set the CLI’s defaults apply: maxErrors=0, maxNPlusOne=0, maxP95Ms=<slow.request.ms>.
-PspiderSense.check.since=before overrides since for one run.
The task depends on nothing, because producing the traffic it judges is the build’s job, as in the test setup below.
Tests
The test task is not attached by default. A test JVM is short-lived and may be forked several times, and several embedded servers would fight over one port. The way to measure tests is to forward to a standalone Spider Sense, which is one line each:
spiderSense {
attachTo.add('test')
collector = 'http://127.0.0.1:4000'
}
./gradlew spiderSense & # once
./gradlew -q spiderSense --args="mark before"
./gradlew test --tests '*OrderServiceTest'
./gradlew -q spiderSense --args="findings --since=before"
With collector set, bootRun forwards too.
That is what is wanted when the tests and the running application should land in one place.
Two applications, several modules
Each module that applies the plugin gets its own block, so in a multi-project build the applications are configured where they are:
// orders/build.gradle
spiderSense { port = 4001 }
// bookstore/build.gradle
spiderSense { port = 4000 }
When one calls the other and the trace should be one trace, run a standalone and forward both:
spiderSense { collector = 'http://127.0.0.1:4000' }
Spring Boot devtools
A devtools restart replaces the application’s classes inside the same JVM. The agent stays attached, the embedded UI keeps running, and the restarted classes are instrumented like the first ones. There is nothing to configure.
A jar that is not on Maven Central
Three ways, for a version that is not published yet or a jar built from a checkout:
./gradlew bootRun -PspiderSense.jar=/home/me/spider-sense/spider-sense-agent/build/libs/spider-sense-0.1.0.jar
spiderSense {
jar = file('/home/me/tools/spider-sense.jar')
}
// In this repository: the jar the build just made, built before bootRun runs.
dependencies {
spiderSense project(path: ':spider-sense-agent', configuration: 'senseJar')
}
./gradlew publishToMavenLocal in this repository publishes both the jar and the plugin to ~/.m2.
mavenLocal() in repositories and in pluginManagement.repositories then makes an unreleased version resolvable like a released one.
Configuration cache
The plugin is compatible with Gradle’s configuration cache.
Every value is a provider, the argument provider holds no Project, and the jar is a FileCollection input.
Coordinates
| Artifact | Coordinates | What it is |
|---|---|---|
The jar |
|
the single distributable jar; its POM declares no dependencies |
The Gradle plugin |
|
the plugin above, id |
Both are published from the Spider Sense repository by ./gradlew publishToMavenLocal into ~/.m2, and, signed, by ./gradlew centralBundle into one bundle for the Central Portal.
The version of both is version in the root gradle.properties, and the plugin’s default version for the jar is that same value, so a plugin and the jar it resolves are always the same release.