Skip to main content

Backend Setup: Java

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

Auto-Installation

If you connected GitHub and received Auto-Installation PRs, the API key, service identifier, and graphql endpoint are already configured for you. Merge the PR and you're done.

Getting Your API Key

  1. Open the Sailfish dashboard
  2. Log in with your enterprise email
  3. Navigate to Settings > Configuration
  4. Copy your company's API key

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:

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

public class Application {
public static void main(String[] args) {
Sailfish.setupInterceptors(SailfishConfig.builder()
.apiKey("<see-api-key-from-your-account-settings-page>")
.serviceIdentifier("acme-corp/java-api/src/main/java/com/acme/Application.java") // Format: <org>/<repo>/<path-to-this-file>
.serviceVersion("1.0.0")
.build());

// Start your application...
}
}

Configuration Options

OptionTypeRequiredDescription
apiKeyStringYesYour Sailfish Enterprise API key
serviceIdentifierStringYesUnique identifier in <org>/<repo>/<path> format
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) {
registry.addInterceptor(new SailfishSpringInterceptor());
}
}

Initialize Sailfish in your @SpringBootApplication main class:

@SpringBootApplication
public class Application {
public static void main(String[] args) {
Sailfish.setupInterceptors(SailfishConfig.builder()
.apiKey("<see-api-key-from-your-account-settings-page>")
.serviceIdentifier("acme-corp/spring-api/src/main/java/com/acme/Application.java") // Format: <org>/<repo>/<path-to-this-file>
.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) {
Sailfish.setupInterceptors(SailfishConfig.builder()
.apiKey("<see-api-key-from-your-account-settings-page>")
.serviceIdentifier("acme-corp/quarkus-api/src/main/java/com/acme/SailfishInit.java") // Format: <org>/<repo>/<path-to-this-file>
.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() {
Sailfish.setupInterceptors(SailfishConfig.builder()
.apiKey("<see-api-key-from-your-account-settings-page>")
.serviceIdentifier("acme-corp/micronaut-api/src/main/java/com/acme/SailfishInit.java") // Format: <org>/<repo>/<path-to-this-file>
.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)
serviceIdentifierNoService identifier
serviceVersionNoService version
excludeMethodsNoSemicolon-separated method names to exclude
excludeConstructorsNoExclude constructors (default: true)
excludeGettersSettersNoExclude getters/setters (default: true)

@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"
));

GIT_SHA

The GIT_SHA environment variable allows Sailfish to correlate telemetry with specific commits.

The SDK auto-detects GIT_SHA from common CI/CD platforms:

CI PlatformEnvironment Variable
GitHub ActionsGITHUB_SHA
GitLab CICI_COMMIT_SHA
CircleCICIRCLE_SHA1
JenkinsGIT_COMMIT
Bitbucket PipelinesBITBUCKET_COMMIT
Travis CITRAVIS_COMMIT
Azure DevOpsBUILD_SOURCEVERSION
AWS CodeBuildCODEBUILD_RESOLVED_SOURCE_VERSION
VercelVERCEL_GIT_COMMIT_SHA

For custom CI systems, set it manually:

# 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("<see-api-key-from-your-account-settings-page>")
.gitSha(System.getenv("GIT_SHA"))
.serviceIdentifier("acme-corp/java-api/src/main/java/com/acme/Application.java") // Format: <org>/<repo>/<path-to-this-file>
.build());

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_ROUTES--Comma-separated routes to skip inbound tracing
SF_DISABLE_PRINT_CAPTUREfalseDisable stdout/stderr capture
PRINT_CONFIGURATION_STATUSESfalsePrint configuration status on startup

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
}
}
}

Docker Compose

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

ARG GIT_SHA
ENV GIT_SHA=$GIT_SHA

ENTRYPOINT ["java", "-javaagent:/app/agent.jar=packages=com.myapp", "-jar", "app.jar"]
# docker-compose.yml
services:
api:
build:
context: .
args:
GIT_SHA: ${GIT_SHA:-$(git rev-parse HEAD)}
ports:
- "8080:8080"

Verifying the Setup

  1. Deploy your application with the Sailfish SDK configured
  2. Trigger some HTTP requests
  3. Open the Sailfish dashboard -- you should see telemetry appearing

Debug Mode

Enable debug output to verify instrumentation is working:

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 API key: Ensure apiKey is set to your Enterprise API key
  2. Check the service identifier: Ensure serviceIdentifier uses the <org>/<repo>/<path> format
  3. 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 errors

  1. Ensure your deployment can reach https://api-service.sailfish.ai
  2. Check that outbound HTTPS (port 443) is not blocked by a firewall or network policy

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, give each a unique serviceIdentifier following the <org>/<repo>/<path> format:

// user-service
Sailfish.setupInterceptors(SailfishConfig.builder()
.apiKey("<see-api-key-from-your-account-settings-page>")
.serviceIdentifier("acme-corp/user-service/src/main/java/com/acme/Application.java") // Format: <org>/<repo>/<path-to-this-file>
.build());

// order-service
Sailfish.setupInterceptors(SailfishConfig.builder()
.apiKey("<ApiKey />")
.serviceIdentifier("acme-corp/order-service/src/main/java/com/acme/Application.java") // Format: <org>/<repo>/<path-to-this-file>
.build());

Use the service filter in the Sailfish dashboard to switch between services.

Next Steps


Local Development

Looking to set up SF Veritas for local development with the Desktop App? See the Desktop App Java guide.