Skip to main content

Backend Setup: C#

This guide walks you through instrumenting an ASP.NET Core application with the SF Veritas SDK. C# instrumentation captures logs, print statements, exceptions, HTTP request/response telemetry, and function execution spans.

Installation

Add the SF Veritas NuGet package to your project:

dotnet add package SailfishAI.SfVeritas

Or add it directly to your .csproj:

<PackageReference Include="SailfishAI.SfVeritas" Version="0.2.0" />

Basic Setup

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

using SailfishAI.SfVeritas;

var builder = WebApplication.CreateBuilder(args);

// Initialize SF Veritas ONLY in development mode
if (builder.Environment.IsDevelopment())
{
builder.Services.AddSailfish(cfg =>
{
cfg.SetApiKey("sf-veritas-local");
cfg.SetGraphqlEndpoint("http://localhost:6776/graphql/");
cfg.SetServiceIdentifier("my-csharp-service");
cfg.SetServiceVersion("1.0.0");
});

builder.Logging.AddSailfishLogger();
}

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseSailfish();
}

app.MapGet("/", () => "Hello World!");

app.Run();
tip

ASP.NET Core sets ASPNETCORE_ENVIRONMENT=Development by default when running with dotnet run. You can also use SF_DEV or any other environment variable as your gate — just make sure instrumentation is off in production.

Configuration Options

OptionTypeRequiredDescription
SetApiKeystringYesUse "sf-veritas-local" for local development
SetGraphqlEndpointstringYesURL of the local collector (default port 6776)
SetServiceIdentifierstringYesUnique name for your service
SetServiceVersionstringNoVersion of your service
SetServiceDisplayNamestringNoDisplay name in the UI
SetGitShastringNoGit commit SHA (auto-detected from .git/HEAD)
SetGitOrgstringNoGit organization (auto-detected from .git/config)
SetGitRepostringNoGit repository name (auto-detected from .git/config)
SetGitProviderstringNoGit provider — "github", "gitlab", "bitbucket" (auto-detected)
SetDomainsToNotPropagateHeadersToList<string>NoDomains to skip header propagation
SetRoutesToSkipNetworkHopsList<string>NoRoutes to skip inbound tracing

What Gets Captured Automatically

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

  • Structured logs — All ILogger calls (LogInformation, LogWarning, LogError, etc.)
  • Print statements — All Console.WriteLine() and Console.Write() output
  • Inbound HTTP requests — Timing, status codes, headers (via UseSailfish() middleware)
  • Outbound HTTP requests — Via SailfishDelegatingHandler in the HttpClient pipeline
  • Exceptions — Unhandled exceptions via AppDomain.UnhandledException + TaskScheduler.UnobservedTaskException
  • Function spans — Automatic endpoint spans for Minimal API routes

DI-Based Setup

SF Veritas integrates with ASP.NET Core's dependency injection. The AddSailfish() extension method handles all service registration:

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
// Registers SailfishConfig, ILoggerProvider, and HttpClient handler
builder.Services.AddSailfish(cfg =>
{
cfg.SetApiKey("sf-veritas-local");
cfg.SetGraphqlEndpoint("http://localhost:6776/graphql/");
cfg.SetServiceIdentifier("my-api");
});

// Optional: add the Sailfish logger provider
builder.Logging.AddSailfishLogger();
}

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
// Adds inbound HTTP middleware
app.UseSailfish();
}

AddSailfish() registers:

  • SailfishConfig as a singleton
  • SailfishLoggerProvider for structured log capture
  • SailfishDelegatingHandler for outbound HTTP tracing
  • A named HttpClient ("SailfishTracked") with the handler pre-configured

Automatic Function Instrumentation

SF Veritas provides two mechanisms for automatic function span capture:

1. DispatchProxy (Interface-Based)

Register services with AddSailfishAutoInstrumentation<TInterface, TImpl>() to automatically wrap every method call with a span:

// Define your service interface
public interface IOrderService
{
Task<Order> GetOrderAsync(string orderId);
Order CreateOrder(CreateOrderRequest request);

[SailfishIgnore] // Exclude from instrumentation
string GetVersion();
}

public class OrderService : IOrderService
{
public async Task<Order> GetOrderAsync(string orderId)
{
// This method is automatically instrumented
return await db.Orders.FindAsync(orderId);
}

public Order CreateOrder(CreateOrderRequest request)
{
// This method is automatically instrumented
return db.Orders.Add(request);
}

public string GetVersion() => "1.0.0"; // Excluded via [SailfishIgnore]
}

Register it in DI:

if (builder.Environment.IsDevelopment())
{
builder.Services.AddSailfishAutoInstrumentation<IOrderService, OrderService>();
}
else
{
builder.Services.AddScoped<IOrderService, OrderService>();
}

This captures:

  • Function name, arguments, and return values
  • Execution timing (nanosecond precision)
  • Async methods (Task, Task<T>, ValueTask, ValueTask<T>)
  • Parent/child span relationships

2. Harmony Runtime Patching

When SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONS=true is set, SF Veritas uses the Harmony library to patch all public methods in your application assemblies at runtime. This provides comprehensive instrumentation without any code changes.

# Enable automatic instrumentation of all user-code methods
SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONS=true dotnet run

Harmony auto-instrumentation:

  • Patches all public methods in non-framework assemblies
  • Skips System.*, Microsoft.*, and other framework namespaces
  • Respects [SailfishIgnore] attributes
  • Captures arguments, return values, and async continuations
  • Uses [ThreadStatic] reentrancy guards to prevent infinite recursion
  • Monitors AppDomain.AssemblyLoad for late-loaded assemblies

Endpoint Auto-Capture

For Minimal API applications, use AddSailfishEndpointFilter() to capture a span for every endpoint in a route group:

// Option 1: Auto-capture all endpoints
var group = app.MapGroup("/api").AddSailfishEndpointFilter();
group.MapGet("/users", GetUsers);
group.MapGet("/orders", GetOrders);

// Option 2: Per-endpoint opt-in
app.MapGet("/api/critical", HandleCritical).AddSailfishEndpointFilter();

Manual Function Tracing

For fine-grained control, use SailfishSpan directly with the using pattern:

Basic Span

using SailfishAI.SfVeritas;

public Order ProcessOrder(string orderId)
{
using var span = SailfishSpan.Start();
var order = db.Orders.Find(orderId);
span.SetReturnValue(order);
return order;
}

SailfishSpan.Start() automatically captures the calling method name, file path, and line number via [CallerMemberName], [CallerFilePath], and [CallerLineNumber] attributes.

Span with Arguments

public Order ProcessOrder(string orderId, decimal amount)
{
using var span = SailfishSpan.Start(
arguments: new Dictionary<string, object?>
{
["orderId"] = orderId,
["amount"] = amount,
});

var result = DoWork(orderId, amount);
span.SetReturnValue(result);
return result;
}

Nested Spans

Spans automatically form parent/child relationships via AsyncLocal<T> context:

public async Task<OrderResult> ProcessOrderAsync(string orderId)
{
using var outerSpan = SailfishSpan.Start();

var validated = await ValidateOrder(orderId);

using (var innerSpan = SailfishSpan.Start("ChargePayment"))
{
await chargePayment(orderId);
innerSpan.SetReturnValue("charged");
}

outerSpan.SetReturnValue(validated);
return validated;
}

Exception Reporting

try
{
await riskyOperation();
}
catch (Exception ex)
{
SailfishVeritas.CaptureException(ex, wasCaught: true);
throw; // Re-throw or handle
}

User Identification

SailfishVeritas.Identify("user-123", new Dictionary<string, object?>
{
["email"] = "user@example.com",
["name"] = "Jane Doe",
["plan"] = "enterprise",
});

Framework Examples

ASP.NET Core Minimal API

using SailfishAI.SfVeritas;

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
builder.Services.AddSailfish(cfg =>
{
cfg.SetApiKey("sf-veritas-local");
cfg.SetGraphqlEndpoint("http://localhost:6776/graphql/");
cfg.SetServiceIdentifier("minimal-api");
});
builder.Logging.AddSailfishLogger();
}

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseSailfish();
}

app.MapGet("/api/users", () =>
{
Console.WriteLine("Fetching users"); // Appears in SF Veritas Console
return Results.Ok(new { users = new string[] {} });
});

app.MapGet("/api/health", () => Results.Ok("ok"));

app.Run();

ASP.NET Core MVC / Controllers

using SailfishAI.SfVeritas;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

if (builder.Environment.IsDevelopment())
{
builder.Services.AddSailfish(cfg =>
{
cfg.SetApiKey("sf-veritas-local");
cfg.SetGraphqlEndpoint("http://localhost:6776/graphql/");
cfg.SetServiceIdentifier("mvc-api");
});
builder.Logging.AddSailfishLogger();
}

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseSailfish();
}

app.MapControllers();
app.Run();

Worker Service / Background Jobs

For non-web applications (background workers, console apps), use SailfishVeritas.Setup() directly:

using SailfishAI.SfVeritas;

if (Environment.GetEnvironmentVariable("SF_DEV") == "true")
{
SailfishVeritas.Setup(
apiKey: "sf-veritas-local",
graphqlEndpoint: "http://localhost:6776/graphql/",
serviceIdentifier: "my-worker"
);
}

// Your application logic...

// Graceful shutdown
SailfishVeritas.Shutdown();

Outbound HTTP Tracing

SF Veritas captures outbound HTTP requests through the SailfishDelegatingHandler. When registered via AddSailfish(), the named client "SailfishTracked" is available via DI:

public class MyService
{
private readonly HttpClient _client;

public MyService(IHttpClientFactory clientFactory)
{
_client = clientFactory.CreateClient("SailfishTracked");
}

public async Task<string> CallExternalApi()
{
var response = await _client.GetAsync("https://api.example.com/data");
return await response.Content.ReadAsStringAsync();
}
}

For ad-hoc usage without DI:

using var client = new HttpClient(new SailfishDelegatingHandler());
var response = await client.GetAsync("https://api.example.com/data");

Environment Variables

VariableDefaultDescription
SF_DEVSet to "true" to enable SF Veritas (alternative gate)
SAILFISH_API_KEYAPI key (alternative to SetApiKey())
SAILFISH_GRAPHQL_ENDPOINThttps://api-service.sailfishqa.com/graphql/Collector endpoint
SERVICE_IDENTIFIERService name (alternative to SetServiceIdentifier())
SF_DEBUGfalseEnable debug logging to stderr
SF_FUNCSPAN_CAPTURE_ARGUMENTSfalseCapture function arguments
SF_FUNCSPAN_CAPTURE_RETURN_VALUEfalseCapture 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_AUTOCAPTURE_ALL_CHILD_FUNCTIONSfalseEnable Harmony runtime auto-instrumentation
SF_FUNCSPAN_CAPTURE_LOCALSfalseCapture method parameters as locals on spans
SF_FUNCSPAN_PARSE_JSON_STRINGSfalseParse JSON strings in captured arguments
SF_NETWORKHOP_CAPTURE_REQUEST_BODYfalseCapture HTTP request bodies
SF_NETWORKHOP_CAPTURE_RESPONSE_BODYfalseCapture HTTP response bodies
SF_NETWORKHOP_CAPTURE_REQUEST_HEADERSfalseCapture HTTP request headers
SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERSfalseCapture HTTP response headers
SF_NETWORKHOP_REQUEST_LIMIT_MB1Max request body capture size in MB
SF_NETWORKHOP_RESPONSE_LIMIT_MB1Max response body capture size in MB
SF_DISABLE_PRINT_CAPTUREfalseDisable stdout/stderr capture
SF_LOG_IGNORE_REGEXRegex pattern to suppress logs from telemetry
SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTESComma-separated routes to skip inbound tracing
SF_NBPOST_GZIP0Set to 1 to enable gzip compression
SF_NBPOST_HTTP20Set to 1 to enable HTTP/2 transmission
SF_NBPOST_BATCH_MAX512Maximum mutations per batch
SF_NBPOST_FLUSH_MS2Flush interval in milliseconds

Configuration File

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

{
"funcspan": {
"default": {
"capture_arguments": true,
"capture_return_value": true,
"sample_rate": 1.0
},
"files": {
"*.cs": {
"capture_arguments": true,
"capture_return_value": true
},
"*Tests.cs": {
"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
}
}
}
}

Supported formats: JSON, TOML, and YAML.

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)
enable_samplingboolEnable sampling for this scope
autocapture_all_childrenboolAuto-capture child functions
capture_sf_veritasboolInclude SF Veritas internals
parse_json_stringsboolParse JSON strings in arguments

Function Configuration

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

Inline Pragmas

You can also configure per-file settings with inline comments in the first 50 lines of a source file:

// sailfish-funcspan: capture_arguments=true, sample_rate=0.5
using SailfishAI.SfVeritas;

public class OrderService
{
// ...
}

Integration Patterns

Launch Profile (Properties/launchSettings.json)

{
"profiles": {
"Development": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Development + SF Veritas Tracing": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONS": "true",
"SF_FUNCSPAN_CAPTURE_ARGUMENTS": "true",
"SF_FUNCSPAN_CAPTURE_RETURN_VALUE": "true"
}
}
}
}

VS Code

Add to .vscode/launch.json:

{
"version": "0.2.0",
"configurations": [
{
"name": "Run with SF Veritas",
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/bin/Debug/net9.0/MyApp.dll",
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONS": "true"
}
}
]
}

JetBrains Rider / Visual Studio

  1. Open Run/Debug Configurations
  2. Add environment variables:
    ASPNETCORE_ENVIRONMENT=Development
    SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONS=true

Docker Compose

# Dockerfile.dev
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /app

COPY *.csproj ./
RUN dotnet restore

COPY . .
RUN dotnet publish -c Debug -o /out

FROM mcr.microsoft.com/dotnet/aspnet:9.0
WORKDIR /app
COPY --from=build /out .

ENV ASPNETCORE_ENVIRONMENT=Development
ENTRYPOINT ["dotnet", "MyApp.dll"]
# docker-compose.yml
services:
api:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "8080:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
# Point to host machine's SF Veritas Desktop App
- SAILFISH_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:
    dotnet run
  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

Debug Mode

Enable debug output to verify instrumentation is working:

SF_DEBUG=true dotnet run

You'll see output like:

[sfveritas] Initialized successfully.
[sfveritas] Harmony auto-instrumentation activated. Patched 42 methods.

Troubleshooting

No logs appearing

  1. Check the desktop app: Ensure the SF Veritas Desktop App is running
  2. Verify the endpoint: Ensure the GraphqlEndpoint matches your server port
  3. Check environment: Ensure ASPNETCORE_ENVIRONMENT=Development is set
  4. Check terminal output: Look for [sfveritas] initialization messages with SF_DEBUG=true

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. For DispatchProxy: ensure services are registered with AddSailfishAutoInstrumentation<TInterface, TImpl>()
  2. For Harmony: set SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONS=true
  3. Check SF_FUNCSPAN_CAPTURE_ARGUMENTS and SF_FUNCSPAN_CAPTURE_RETURN_VALUE are enabled

Port conflicts

ASP.NET Core's launchSettings.json can override your configured port. Use --no-launch-profile to avoid this:

dotnet run --no-launch-profile

Multi-Service Setup

When running multiple .NET services locally, give each a unique ServiceIdentifier:

// user-service/Program.cs
builder.Services.AddSailfish(cfg =>
{
cfg.SetApiKey("sf-veritas-local");
cfg.SetGraphqlEndpoint("http://localhost:6776/graphql/");
cfg.SetServiceIdentifier("user-service");
});

// order-service/Program.cs
builder.Services.AddSailfish(cfg =>
{
cfg.SetApiKey("sf-veritas-local");
cfg.SetGraphqlEndpoint("http://localhost:6776/graphql/");
cfg.SetServiceIdentifier("order-service");
});

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

Next Steps