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();
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
| Option | Type | Required | Description |
|---|---|---|---|
SetApiKey | string | Yes | Use "sf-veritas-local" for local development |
SetGraphqlEndpoint | string | Yes | URL of the local collector (default port 6776) |
SetServiceIdentifier | string | Yes | Unique name for your service |
SetServiceVersion | string | No | Version of your service |
SetServiceDisplayName | string | No | Display name in the UI |
SetGitSha | string | No | Git commit SHA (auto-detected from .git/HEAD) |
SetGitOrg | string | No | Git organization (auto-detected from .git/config) |
SetGitRepo | string | No | Git repository name (auto-detected from .git/config) |
SetGitProvider | string | No | Git provider — "github", "gitlab", "bitbucket" (auto-detected) |
SetDomainsToNotPropagateHeadersTo | List<string> | No | Domains to skip header propagation |
SetRoutesToSkipNetworkHops | List<string> | No | Routes to skip inbound tracing |
What Gets Captured Automatically
Once AddSailfish is called, these are captured with zero additional code:
- Structured logs — All
ILoggercalls (LogInformation,LogWarning,LogError, etc.) - Print statements — All
Console.WriteLine()andConsole.Write()output - Inbound HTTP requests — Timing, status codes, headers (via
UseSailfish()middleware) - Outbound HTTP requests — Via
SailfishDelegatingHandlerin theHttpClientpipeline - 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:
SailfishConfigas a singletonSailfishLoggerProviderfor structured log captureSailfishDelegatingHandlerfor 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.AssemblyLoadfor 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
| Variable | Default | Description |
|---|---|---|
SF_DEV | — | Set to "true" to enable SF Veritas (alternative gate) |
SAILFISH_API_KEY | — | API key (alternative to SetApiKey()) |
SAILFISH_GRAPHQL_ENDPOINT | https://api-service.sailfishqa.com/graphql/ | Collector endpoint |
SERVICE_IDENTIFIER | — | Service name (alternative to SetServiceIdentifier()) |
SF_DEBUG | false | Enable debug logging to stderr |
SF_FUNCSPAN_CAPTURE_ARGUMENTS | false | Capture function arguments |
SF_FUNCSPAN_CAPTURE_RETURN_VALUE | false | Capture return values |
SF_FUNCSPAN_ARG_LIMIT_MB | 1 | Max argument capture size in MB |
SF_FUNCSPAN_RETURN_LIMIT_MB | 1 | Max return value capture size in MB |
SF_FUNCSPAN_ENABLE_SAMPLING | false | Enable span sampling |
SF_FUNCSPAN_SAMPLE_RATE | 1.0 | Function span sampling rate (0.0 to 1.0) |
SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONS | false | Enable Harmony runtime auto-instrumentation |
SF_FUNCSPAN_CAPTURE_LOCALS | false | Capture method parameters as locals on spans |
SF_FUNCSPAN_PARSE_JSON_STRINGS | false | Parse JSON strings in captured arguments |
SF_NETWORKHOP_CAPTURE_REQUEST_BODY | false | Capture HTTP request bodies |
SF_NETWORKHOP_CAPTURE_RESPONSE_BODY | false | Capture HTTP response bodies |
SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS | false | Capture HTTP request headers |
SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS | false | Capture HTTP response headers |
SF_NETWORKHOP_REQUEST_LIMIT_MB | 1 | Max request body capture size in MB |
SF_NETWORKHOP_RESPONSE_LIMIT_MB | 1 | Max response body capture size in MB |
SF_DISABLE_PRINT_CAPTURE | false | Disable stdout/stderr capture |
SF_LOG_IGNORE_REGEX | — | Regex pattern to suppress logs from telemetry |
SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTES | — | Comma-separated routes to skip inbound tracing |
SF_NBPOST_GZIP | 0 | Set to 1 to enable gzip compression |
SF_NBPOST_HTTP2 | 0 | Set to 1 to enable HTTP/2 transmission |
SF_NBPOST_BATCH_MAX | 512 | Maximum mutations per batch |
SF_NBPOST_FLUSH_MS | 2 | Flush 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
| Option | Type | Description |
|---|---|---|
capture_arguments | bool | Capture function arguments |
capture_return_value | bool | Capture return values |
arg_limit_mb | int | Max argument size in MB |
return_limit_mb | int | Max return value size in MB |
sample_rate | float | Sampling rate (0.0 to 1.0) |
enable_sampling | bool | Enable sampling for this scope |
autocapture_all_children | bool | Auto-capture child functions |
capture_sf_veritas | bool | Include SF Veritas internals |
parse_json_strings | bool | Parse 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
- Open Run/Debug Configurations
- 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/
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
- Start your application:
dotnet run - Open the SF Veritas Desktop App
- Open the Console panel — you should see logs and print statements
- Trigger some HTTP requests — you should see them in the Network panel
- 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
- Check the desktop app: Ensure the SF Veritas Desktop App is running
- Verify the endpoint: Ensure the
GraphqlEndpointmatches your server port - Check environment: Ensure
ASPNETCORE_ENVIRONMENT=Developmentis set - Check terminal output: Look for
[sfveritas]initialization messages withSF_DEBUG=true
Connection refused errors
- Verify the SF Veritas Desktop App is installed and running
- Check that the local server is running (look for server status in the Desktop App)
- Ensure port 6776 is not blocked by a firewall
- In Docker: use
host.docker.internalinstead oflocalhost
No function spans in Flamechart
- For DispatchProxy: ensure services are registered with
AddSailfishAutoInstrumentation<TInterface, TImpl>() - For Harmony: set
SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONS=true - Check
SF_FUNCSPAN_CAPTURE_ARGUMENTSandSF_FUNCSPAN_CAPTURE_RETURN_VALUEare 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.