Skip to main content

Backend Setup: Java

This guide walks you through instrumenting a Java application with the SF Veritas SDK. Java instrumentation captures logs, print statements, exceptions, HTTP request/response telemetry, and function execution spans.

Installation

Maven

Add the Sailfish repository and SDK dependency to your pom.xml:

<repositories>
<repository>
<id>sailfish</id>
<url>https://us-central1-maven.pkg.dev/sailfish-ai/sailfish-maven</url>
</repository>
</repositories>

<dependencies>
<dependency>
<groupId>ai.sailfish</groupId>
<artifactId>sf-veritas</artifactId>
<version>0.1.0</version>
</dependency>
<!-- Optional: automatic function instrumentation -->
<dependency>
<groupId>ai.sailfish</groupId>
<artifactId>sf-veritas-agent</artifactId>
<version>0.1.0</version>
<scope>runtime</scope>
</dependency>
</dependencies>

Gradle (Groovy)

repositories {
maven {
url "https://us-central1-maven.pkg.dev/sailfish-ai/sailfish-maven"
}
}

dependencies {
implementation 'ai.sailfish:sf-veritas:0.1.0'
runtimeOnly 'ai.sailfish:sf-veritas-agent:0.1.0' // Optional
}

Gradle (Kotlin DSL)

repositories {
maven {
url = uri("https://us-central1-maven.pkg.dev/sailfish-ai/sailfish-maven")
}
}

dependencies {
implementation("ai.sailfish:sf-veritas:0.1.0")
runtimeOnly("ai.sailfish:sf-veritas-agent:0.1.0") // Optional
}

Basic Setup

Add the following to your application's startup code. Use an environment variable check so instrumentation only runs during local development:

import com.sailfish.veritas.Sailfish;
import com.sailfish.veritas.SailfishConfig;

public class Application {
public static void main(String[] args) {
// Initialize SF Veritas ONLY in development mode
if ("true".equalsIgnoreCase(System.getenv("SF_DEV"))) {
Sailfish.setupInterceptors(SailfishConfig.builder()
.apiKey("sf-veritas-local")
.graphqlEndpoint("http://localhost:6776/graphql/")
.serviceIdentifier("my-java-service")
.serviceVersion("1.0.0")
.build());
}

// Start your application...
}
}

Configuration Options

OptionTypeRequiredDescription
apiKeyStringYesUse "sf-veritas-local" for local development
graphqlEndpointStringYesURL of the local collector (default port 6776)
serviceIdentifierStringYesUnique name for your service
serviceVersionStringNoVersion of your service
serviceDisplayNameStringNoDisplay name in the UI
gitShaStringNoGit commit SHA
gitOrgStringNoGit organization
gitRepoStringNoGit repository name
gitProviderStringNoGit provider (e.g., "github")
domainsToNotPropagateHeadersToList<String>NoDomains to skip header propagation
routesToSkipNetworkHopsList<String>NoRoutes to skip inbound tracing

What Gets Captured Automatically

Once setupInterceptors is called, these are captured with zero additional code:

  • Structured logs — All SLF4J, Logback, Log4j2, JUL, and JBoss Logger calls
  • Print statements — All System.out.println() and System.err.println() output
  • Exceptions — Manually reported exceptions with Sailfish.transmitException()
  • Outbound HTTP requests — Via java.net.http.HttpClient default transport
  • Inbound HTTP requests — Via framework-specific filters (see below)

Framework Integration

Spring Boot

Register the SailfishSpringInterceptor via a WebMvcConfigurer:

import com.sailfish.veritas.network.inbound.SailfishSpringInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
if ("true".equalsIgnoreCase(System.getenv("SF_DEV"))) {
registry.addInterceptor(new SailfishSpringInterceptor());
}
}
}

Initialize Sailfish in your @SpringBootApplication main class:

@SpringBootApplication
public class Application {
public static void main(String[] args) {
if ("true".equalsIgnoreCase(System.getenv("SF_DEV"))) {
Sailfish.setupInterceptors(SailfishConfig.builder()
.apiKey("sf-veritas-local")
.graphqlEndpoint("http://localhost:6776/graphql/")
.serviceIdentifier("my-spring-service")
.build());
}
SpringApplication.run(Application.class, args);
}
}

Jakarta Servlet

For Jakarta Servlet 6.0+ containers (Jetty 12, Tomcat 10.1+), register the SailfishServletFilter.JakartaFilter:

import com.sailfish.veritas.network.inbound.SailfishServletFilter;
import jakarta.servlet.DispatcherType;
import java.util.EnumSet;

// Programmatic registration (embedded Jetty example)
FilterHolder sailfishFilter = new FilterHolder(new SailfishServletFilter.JakartaFilter());
context.addFilter(sailfishFilter, "/*", EnumSet.of(DispatcherType.REQUEST));

For web.xml registration:

<filter>
<filter-name>SailfishFilter</filter-name>
<filter-class>com.sailfish.veritas.network.inbound.SailfishServletFilter$JakartaFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>SailfishFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

JAX-RS (Jersey, RESTEasy)

Register SailfishJaxRsFilter.JakartaFilter in your JAX-RS Application:

import com.sailfish.veritas.network.inbound.SailfishJaxRsFilter;
import jakarta.ws.rs.core.Application;
import java.util.Set;

public class MyApp extends Application {
@Override
public Set<Class<?>> getClasses() {
return Set.of(
MyResource.class,
SailfishJaxRsFilter.JakartaFilter.class
);
}
}

Or with Jersey ResourceConfig:

ResourceConfig config = new ResourceConfig();
config.register(SailfishJaxRsFilter.JakartaFilter.class);
config.register(MyResource.class);

Quarkus

Quarkus uses Jakarta REST (RESTEasy Reactive). Create a @Provider class that extends the Sailfish filter:

import com.sailfish.veritas.network.inbound.SailfishJaxRsFilter;
import jakarta.ws.rs.ext.Provider;

@Provider
public class SailfishFilterProvider extends SailfishJaxRsFilter.JakartaFilter {
}

Initialize Sailfish on startup:

import com.sailfish.veritas.Sailfish;
import com.sailfish.veritas.SailfishConfig;
import io.quarkus.runtime.StartupEvent;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;

@ApplicationScoped
public class SailfishInit {
void onStart(@Observes StartupEvent ev) {
if ("true".equalsIgnoreCase(System.getenv("SF_DEV"))) {
Sailfish.setupInterceptors(SailfishConfig.builder()
.apiKey("sf-veritas-local")
.graphqlEndpoint("http://localhost:6776/graphql/")
.serviceIdentifier("my-quarkus-service")
.build());
}
}
}

Micronaut

The SailfishMicronautFilter is auto-discovered by Micronaut when sf-veritas is on the classpath. It uses the @Filter("/**") annotation for automatic registration.

Initialize Sailfish with a @Context @Singleton bean:

import com.sailfish.veritas.Sailfish;
import com.sailfish.veritas.SailfishConfig;
import io.micronaut.context.annotation.Context;
import jakarta.annotation.PostConstruct;
import jakarta.inject.Singleton;

@Context
@Singleton
public class SailfishInit {
@PostConstruct
void init() {
if ("true".equalsIgnoreCase(System.getenv("SF_DEV"))) {
Sailfish.setupInterceptors(SailfishConfig.builder()
.apiKey("sf-veritas-local")
.graphqlEndpoint("http://localhost:6776/graphql/")
.serviceIdentifier("my-micronaut-service")
.build());
}
}
}

No additional filter registration is needed — Micronaut discovers it automatically.

Vert.x

Register the SailfishVertxHandler as a route handler before your application routes:

import com.sailfish.veritas.network.inbound.SailfishVertxHandler;
import io.vertx.core.Vertx;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.BodyHandler;

Vertx vertx = Vertx.vertx();
Router router = Router.router(vertx);

// Body handler must be before Sailfish handler for request body capture
router.route().handler(BodyHandler.create());

// Register Sailfish handler for inbound HTTP tracing
router.route().handler(new SailfishVertxHandler());

// Your routes
router.get("/api/users").handler(ctx -> {
ctx.json(Map.of("users", List.of()));
});

vertx.createHttpServer()
.requestHandler(router)
.listen(8080);

Dropwizard

Dropwizard 4.x uses Jetty + Jersey (Jakarta namespace). Register the Sailfish JAX-RS filter in your Application's run() method:

import com.sailfish.veritas.network.inbound.SailfishJaxRsFilter;
import io.dropwizard.core.Application;
import io.dropwizard.core.setup.Environment;

public class MyApp extends Application<MyConfig> {
@Override
public void run(MyConfig config, Environment environment) {
// Register Sailfish filter
environment.jersey().register(new SailfishJaxRsFilter.JakartaFilter());

// Register your resources
environment.jersey().register(new MyResource());
}
}

Play Framework

Not Currently Supported

Play Framework is not currently supported by the Sailfish Java SDK. Play uses sbt as its build system and has a custom classloading architecture that requires dedicated integration work. If you need Play support, please contact us at support@sailfishqa.com.

Automatic Function Instrumentation (ByteBuddy Agent)

For full function-level tracing in the Flamechart — including automatic argument capture and return values — use the sf-veritas-agent Java agent.

How It Works

The agent uses ByteBuddy to intercept method entry and exit at class load time. For every method in your configured packages, it:

  1. Method entry — Records the method name, class, and captures all argument values
  2. Method exit — Records timing and return value, ends the span
  3. Exception thrown — Captures the exception and ends the span with error

Your source files are never modified. Instrumentation happens at class load time via the JVM agent mechanism.

Usage

java -javaagent:sf-veritas-agent-0.1.0.jar=packages=com.myapp -jar myapp.jar

Agent Arguments

ArgumentRequiredDescription
packagesYesSemicolon-separated packages to instrument (e.g., com.myapp;com.shared)
apiKeyNoAPI key (can also be set via SailfishConfig or env var)
graphqlEndpointNoCustom endpoint URL
serviceIdentifierNoService identifier
serviceVersionNoService version
excludeMethodsNoSemicolon-separated method names to exclude
excludeConstructorsNoExclude constructors (default: true)
excludeGettersSettersNoExclude getters/setters (default: true)

Example with Maven

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-javaagent:${settings.localRepository}/ai/sailfish/sf-veritas-agent/0.1.0/sf-veritas-agent-0.1.0.jar=packages=com.myapp</argLine>
</configuration>
</plugin>

@CaptureSpan Annotation

For selective instrumentation, annotate individual methods with @CaptureSpan:

import com.sailfish.veritas.span.CaptureSpan;

public class OrderService {

@CaptureSpan
public String processOrder(Order order) {
// Method execution is timed and profiled
return "processed";
}

@CaptureSpan(captureArgs = false, captureReturn = false)
public void sensitiveMethod(String secret) {
// Only timing is captured, no argument/return data
}

@CaptureSpan(name = "custom-span-name", sampleRate = 0.5)
public void highFrequencyMethod() {
// Custom name and 50% sampling
}
}
AttributeTypeDefaultDescription
captureArgsbooleantrueCapture method arguments
captureReturnbooleantrueCapture return value
argLimitMBdouble-1 (global default)Max argument serialization size in MB
returnLimitMBdouble-1 (global default)Max return value serialization size in MB
nameString"" (method name)Custom span name
sampleRatedouble-1 (global default)Sampling rate (0.0–1.0)
note

@CaptureSpan requires the ByteBuddy agent to be loaded. Without the agent, the annotation has no effect.

Manual Function Tracing

For fine-grained control over which functions are traced, use the SailfishSpan API:

Basic Span

import com.sailfish.veritas.span.SailfishSpan;

public String processOrder(String orderId) {
SailfishSpan span = SailfishSpan.start("processOrder");
try {
String result = doWork(orderId);
span.end(result);
return result;
} catch (Exception e) {
span.endWithError(e);
throw e;
}
}

Span with Arguments

SailfishSpan span = SailfishSpan.start("processOrder", Map.of(
"orderId", orderId,
"amount", amount
));
try {
// ... function body ...
span.end(result);
} catch (Exception e) {
span.endWithError(e);
throw e;
}

Exception Reporting

import com.sailfish.veritas.Sailfish;

try {
riskyOperation();
} catch (Exception e) {
Sailfish.transmitException(e);
// Handle or rethrow
}

User Identification

Sailfish.identify("user-123", Map.of(
"email", "user@example.com",
"name", "Jane Doe",
"plan", "enterprise"
));

Environment Variables

VariableDefaultDescription
SF_DEBUGfalseEnable debug logging to stderr
SF_LOG_IGNORE_REGEX(health check pattern)Regex to suppress matching log messages
SF_FUNCSPAN_CAPTURE_ARGUMENTStrueCapture function arguments
SF_FUNCSPAN_CAPTURE_RETURN_VALUEtrueCapture return values
SF_FUNCSPAN_ARG_LIMIT_MB1Max argument capture size in MB
SF_FUNCSPAN_RETURN_LIMIT_MB1Max return value capture size in MB
SF_FUNCSPAN_ENABLE_SAMPLINGfalseEnable span sampling
SF_FUNCSPAN_SAMPLE_RATE1.0Function span sampling rate (0.0 to 1.0)
SF_FUNCSPAN_CAPTURE_SF_VERITASfalseCapture spans for Sailfish SDK internals
SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONStrueAuto-capture all child function calls
SF_NETWORKHOP_CAPTURE_ENABLEDtrueEnable inbound/outbound HTTP tracing
SF_NETWORKHOP_CAPTURE_REQUEST_HEADERSfalseCapture HTTP request headers
SF_NETWORKHOP_CAPTURE_REQUEST_BODYfalseCapture HTTP request bodies
SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERSfalseCapture HTTP response headers
SF_NETWORKHOP_CAPTURE_RESPONSE_BODYfalseCapture HTTP response bodies
SF_NETWORKHOP_REQUEST_LIMIT_MB1Max request body capture size in MB
SF_NETWORKHOP_RESPONSE_LIMIT_MB1Max response body capture size in MB
SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTESComma-separated routes to skip inbound tracing
SF_DISABLE_PRINT_CAPTUREfalseDisable stdout/stderr capture
PRINT_CONFIGURATION_STATUSESfalsePrint configuration status on startup
SAILFISH_GRAPHQL_ENDPOINTAlternative to SailfishConfig.graphqlEndpoint

Configuration File

Create a .sailfish file in your project root for per-file and per-function span configuration:

{
"files": {
"*.java": {
"capture_arguments": true,
"capture_return_value": true,
"sample_rate": 1.0
},
"*Test.java": {
"sample_rate": 0.0
}
},
"functions": {
"processOrder": {
"capture_arguments": true,
"capture_return_value": true,
"arg_limit_mb": 2,
"return_limit_mb": 2
},
"healthCheck": {
"sample_rate": 0.0
}
}
}

File Configuration

OptionTypeDescription
capture_argumentsboolCapture function arguments
capture_return_valueboolCapture return values
arg_limit_mbintMax argument size in MB
return_limit_mbintMax return value size in MB
sample_ratefloatSampling rate (0.0 to 1.0)

Function Configuration

Same options as file configuration but applied to specific function names. Function-level config takes priority over file-level config.

Docker Compose

For local development with Docker Compose, include the agent JAR in your build and pass -javaagent as a JVM argument:

FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -q
COPY src/ src/
RUN mvn clean package -DskipTests -q

FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /app/target/myapp.jar /app/app.jar
COPY --from=builder /app/target/dependency/sf-veritas-agent-0.1.0.jar /app/agent.jar

ENV SF_DEV=true
ENTRYPOINT ["java", "-javaagent:/app/agent.jar=packages=com.myapp", "-jar", "app.jar"]
# docker-compose.yml
services:
api:
build: .
ports:
- "8080:8080"
environment:
- SF_DEV=true
- SF_GRAPHQL_ENDPOINT=http://host.docker.internal:6776/graphql/
Docker networking

Use host.docker.internal to reach the SF Veritas Desktop App running on your host machine. This works on Docker Desktop for Mac and Windows. On Linux, add extra_hosts: ["host.docker.internal:host-gateway"] to your service.

Verifying the Setup

  1. Start your application with SF Veritas enabled
  2. Open the SF Veritas Desktop App
  3. Open the Console panel — you should see logs and print statements
  4. Trigger some HTTP requests — you should see them in the Network panel
  5. Open the Flamechart — you should see function execution traces (requires agent)

Debug Mode

Enable debug output to verify instrumentation is working:

SF_DEV=true SF_DEBUG=true java -javaagent:sf-veritas-agent.jar=packages=com.myapp -jar myapp.jar

You'll see output like:

[Sailfish Veritas] Agent loaded — instrumenting packages: [com.myapp]
[Sailfish] setupInterceptors() called
[Sailfish] Log capture initialized
[Sailfish] Print capture initialized

Troubleshooting

No logs appearing

  1. Check the desktop app: Ensure the SF Veritas Desktop App is running
  2. Verify the endpoint: Ensure graphqlEndpoint matches your server port
  3. Check SF_DEV: Must be set to "true"
  4. Check terminal output: Look for [Sailfish] initialization messages

Agent not loading

  1. Verify the agent JAR path: java -javaagent:/path/to/sf-veritas-agent-0.1.0.jar=packages=com.myapp
  2. Check the packages argument — it must match your application's package prefix
  3. Enable debug: SF_DEBUG=true to see which classes are being instrumented

Connection refused errors

  1. Verify the SF Veritas Desktop App is installed and running
  2. Check that the local server is running (look for server status in the Desktop App)
  3. Ensure port 6776 is not blocked by a firewall
  4. In Docker: use host.docker.internal instead of localhost

No function spans in Flamechart

  1. Ensure the ByteBuddy agent is loaded (check for [Sailfish Veritas] Agent loaded in output)
  2. Verify the packages argument includes your application package
  3. Check SF_FUNCSPAN_CAPTURE_ARGUMENTS and SF_FUNCSPAN_CAPTURE_RETURN_VALUE are not disabled

Multi-Service Setup

When running multiple Java services locally, give each a unique serviceIdentifier:

// user-service
Sailfish.setupInterceptors(SailfishConfig.builder()
.apiKey("sf-veritas-local")
.graphqlEndpoint("http://localhost:6776/graphql/")
.serviceIdentifier("user-service")
.build());

// order-service
Sailfish.setupInterceptors(SailfishConfig.builder()
.apiKey("sf-veritas-local")
.graphqlEndpoint("http://localhost:6776/graphql/")
.serviceIdentifier("order-service")
.build());

Use the service filter in the Console to switch between services.

Enterprise Setup

Enterprise Only

This section applies to users with a Sailfish Enterprise account. If you're using the Desktop App for local development only, the basic setup above is all you need.

Enterprise users need two configurations — one for local development (Desktop App) and one for staging/production (Sailfish cloud). The SDK should detect which environment it's running in and configure itself accordingly.

Dual-Mode Configuration

import com.sailfish.veritas.Sailfish;
import com.sailfish.veritas.SailfishConfig;

public class Application {
public static void main(String[] args) {
String sfDev = System.getenv("SF_DEV");
String apiKey = System.getenv("SAILFISH_API_KEY");

if ("true".equalsIgnoreCase(sfDev)) {
// Local development — send to Desktop App
Sailfish.setupInterceptors(SailfishConfig.builder()
.apiKey("sf-veritas-local")
.graphqlEndpoint("http://localhost:6776/graphql/")
.serviceIdentifier("my-java-service")
.serviceVersion("1.0.0")
.build());
} else if (apiKey != null && !apiKey.isEmpty()) {
// Staging/Production — send to Sailfish cloud
// Do NOT set graphqlEndpoint — the SDK defaults to the cloud endpoint
Sailfish.setupInterceptors(SailfishConfig.builder()
.apiKey(apiKey)
.serviceIdentifier("my-java-service")
.serviceVersion("1.0.0")
.build());
}

// Start your application...
}
}

Spring Boot Example

@SpringBootApplication
public class Application {
public static void main(String[] args) {
String sfDev = System.getenv("SF_DEV");
String apiKey = System.getenv("SAILFISH_API_KEY");

if ("true".equalsIgnoreCase(sfDev)) {
Sailfish.setupInterceptors(SailfishConfig.builder()
.apiKey("sf-veritas-local")
.graphqlEndpoint("http://localhost:6776/graphql/")
.serviceIdentifier("my-spring-service")
.build());
} else if (apiKey != null && !apiKey.isEmpty()) {
Sailfish.setupInterceptors(SailfishConfig.builder()
.apiKey(apiKey)
.serviceIdentifier("my-spring-service")
.build());
}

SpringApplication.run(Application.class, args);
}
}

Environment Variables for Enterprise

Set these in your staging/production deployment:

# Required — your company's Enterprise API key
SAILFISH_API_KEY=your-enterprise-api-key

# Required — the git commit SHA for this build
GIT_SHA=$(git rev-parse HEAD)

# Optional — override the endpoint (only for local dev, do NOT set in production)
# SAILFISH_GRAPHQL_ENDPOINT=http://localhost:6776/graphql/

GIT_SHA

The GIT_SHA environment variable allows Sailfish to correlate telemetry with specific commits. Set it in your build pipeline:

# Docker
docker build --build-arg GIT_SHA=$(git rev-parse HEAD) .

# In Dockerfile
ARG GIT_SHA
ENV GIT_SHA=$GIT_SHA

# Maven (pass as system property)
mvn package -Dgit.sha=$(git rev-parse HEAD)

# Gradle
./gradlew build -Pgit.sha=$(git rev-parse HEAD)

You can also pass it directly to the SDK:

Sailfish.setupInterceptors(SailfishConfig.builder()
.apiKey(apiKey)
.gitSha(System.getenv("GIT_SHA"))
.serviceIdentifier("my-java-service")
.build());

URL Override

You can override the GraphQL endpoint in two ways:

  1. Environment variable: SAILFISH_GRAPHQL_ENDPOINT
  2. Constructor argument: graphqlEndpoint in SailfishConfig.builder()
EnvironmentEndpointHow
Local devhttp://localhost:6776/graphql/Set graphqlEndpoint() or SAILFISH_GRAPHQL_ENDPOINT
Staging/ProductionDefault (cloud)Do not set either — the SDK uses https://api-service.sailfishqa.com/graphql/ automatically

Next Steps