← Blog

Getting started with Obvyr takes about five minutes

Every observability tool I’ve evaluated has asked me to do something I wasn’t already doing. A new command to learn. A wrapper script to maintain. A separate reporting step bolted onto the end of the pipeline. The friction is small at first, but it compounds. Tools that require behaviour change don’t get adopted.

The ones that do get adopted tend to share a quality: they make existing workflows visible rather than replacing them. They slot in, they observe, and they stay out of the way. That’s what we tried to build with Obvyr’s collectors.

There are currently two: a Gradle plugin for JVM projects, and a CLI that works with any language or framework. Both are thin wrappers around Obvyr’s public collect API, which means you can also integrate directly if neither fits your stack. Here’s how each one works.

The Gradle plugin

Add the plugin to your build.gradle.kts:

plugins {
    id("com.obvyr.gradle") version "1.0.0"
}

Create a CLI agent in your Obvyr project, copy the token it generates, and export it as an environment variable:

export OBVYR_API_KEY="agt_your_token_here"

Then run your tests exactly as you normally would:

./gradlew test

The plugin hooks into Gradle’s test task lifecycle. When the task finishes, it packages the JUnit XML output that Gradle already produces, adds execution metadata, and submits everything to Obvyr. You don’t change the command. You don’t change the pipeline. There’s no separate reporting step.

Beyond the agent key, a handful of optional settings shape what gets recorded. The user field identifies the execution context: not an individual developer’s identity, but something like local-dev, github-ci, or staging-eu. Tags let you label observations in ways that make sense for how your team works: the branch type, the module under test, the environment.

obvyr {
    user = "local-dev"
    tags = listOf("unit", "feature-branch")
}

For multi-module projects, you can apply the plugin once at the root level and let it propagate, or configure each module separately to give its observations distinct tags. In CI, configuration typically comes entirely from environment variables, with no DSL changes needed:

- name: Run tests
  env:
    OBVYR_API_KEY: ${{ secrets.OBVYR_API_KEY }}
    OBVYR_USER: github-ci
    OBVYR_TAGS: ci,unit
  run: ./gradlew test

Add OBVYR_API_KEY to your repository secrets, and that’s the CI integration complete. Most teams see their first observation in Obvyr within five minutes of adding the plugin.

The CLI

The CLI works with any test command in any language. We recommend installing it with uvx for CI environments, because it runs without a persistent installation and avoids version conflicts with your project’s own dependencies:

uvx obvyr-cli --version

For local development, pipx is a better fit:

pipx install obvyr-cli

Either way, the usage is the same: prefix your existing test command with obvyr-cli.

obvyr-cli pytest tests/
obvyr-cli npm test
obvyr-cli bundle exec rspec
obvyr-cli dotnet test

The CLI captures the command, its arguments, the full output, the exit code, and how long it took. If you also want JUnit XML results broken out individually, giving you pass counts, failure counts, and the output from specific failing tests rather than the entire run lumped together, configure an attachment path pointing at the report file your test runner already produces:

OBVYR_PROFILES__DEFAULT__ATTACHMENT_PATHS="test-results/**/*.xml" \
  obvyr-cli pytest tests/ --junitxml=test-results/results.xml

We’ve standardised on JUnit XML as the format for structured test results. It originated with JUnit but has since become the de facto interchange format for test output across the industry: pytest, Jest, RSpec, NUnit, xUnit, MSTest, and most other mainstream frameworks can all produce it, even when they don’t do so by default. That common format is what lets Obvyr parse individual test results regardless of which language or framework produced them. If your framework of choice doesn’t emit JUnit XML out of the box, it almost certainly supports it with a reporter flag or a lightweight plugin.

The CLI’s configuration system supports multiple named profiles, which is useful when you’re working across different environments or projects. For local development, a .env file in your project root is the most convenient way to configure it:

OBVYR_PROFILES__DEFAULT__API_KEY=agt_your_token_here
OBVYR_CLI_USER=local-dev
OBVYR_PROFILES__DEFAULT__TAGS=local
OBVYR_PROFILES__DEFAULT__ATTACHMENT_PATHS=test-results/**/*.xml

In CI, you’d set the same values as actual environment variables, keeping the token out of version control. And if the Obvyr API is unreachable (a network issue, a misconfigured token, a maintenance window), the CLI logs a warning and exits with the original command’s exit code. Your build passes or fails on its own merits; Obvyr never blocks it.

If you’d prefer a step-by-step walkthrough that covers project setup, agent creation, and your first observation from scratch, the getting started guide takes you through the whole process.

The collect API

The Gradle plugin and the CLI are both thin wrappers around a single endpoint: POST https://api.obvyr.com/collect. The API is public and documented, which means you can integrate Obvyr directly from any language or build system without waiting for a native integration to exist.

The request is a multipart upload, authenticated with your agent token as a Bearer credential. The payload is a zstd-compressed tar archive containing a command.json file with the execution metadata, an optional output.txt carrying the command’s stdout and stderr, and an optional attachment/ directory for files like JUnit XML reports or coverage data.

The command.json is straightforward:

{
  "command": ["pytest", "tests/"],
  "user": "github-ci",
  "return_code": 0,
  "execution_time_ms": 4217,
  "tags": ["ci", "unit"]
}

The full request format, archive structure, and response shape are covered in the API documentation. If you’re building an integration for a language or build system we don’t yet have a native wrapper for, this is where to start.

Other stacks

The Gradle plugin and the CLI are the first two integrations, and they won’t be the last. We’re actively looking to expand native support to other build systems and test frameworks. Maven, Bazel, and others are on the list, and we’re keen to build wrappers and SDKs for any stack where teams are doing serious testing work.

If your team is using something we don’t yet support natively, we’d genuinely like to hear about it. The more we understand where people want to use Obvyr, the better we can prioritise what to build. You can reach us at hello@obvyr.com, and if you’ve already built something against the collect API and want to share it, even better.

What you see

After the first run, you’ll see an observation in your project: the command, whether it passed, how long it took, and the full output. If JUnit XML was attached, the individual test results are available separately: each test, its status, and the output from any that failed.

That first observation isn’t very interesting on its own. What becomes interesting is the second one, and the tenth, and the hundredth. Once the data accumulates, patterns that aren’t visible in a single run start to emerge: tests that fail only in CI, tests that are getting slower over time, tests that fail intermittently without a consistent cause. That’s what Obvyr is actually for, but you can’t get there without starting somewhere.

Try it

Obvyr is open to everyone with a 14-day free trial. If your team runs automated tests and you want to understand what that data is worth, sign up and give it a try.

Stay in the loop