Backend Setup: PHP
This guide walks you through instrumenting a PHP application with the SF Veritas SDK. PHP instrumentation captures logs, exceptions with stack traces, function execution spans, outbound HTTP requests, and print/echo output.
Installation
1. Add the Sailfish Repository
Add the Sailfish Composer repository to your composer.json:
{
"repositories": [
{
"type": "composer",
"url": "https://satis.sailfishqa.com/repo"
}
]
}
2. Install the Package
composer require sailfish/sf-veritas
- PHP 8.1 or higher
ext-curlextension (usually enabled by default)ext-jsonextension (usually enabled by default)
Basic Setup
Add the following to your application's entry point. The SDK should be activated only during local development — wrap it in a debug/environment check:
SF Veritas should only run in local development. Use your framework's debug flag or an environment variable to control activation. This ensures zero overhead in production.
<?php
use Sailfish\SfVeritas\SetupInterceptors;
// Initialize SF Veritas ONLY in development mode
if (getenv('APP_DEBUG') === 'true') {
require_once __DIR__ . '/vendor/autoload.php';
SetupInterceptors::init([
'api_key' => 'sf-veritas-local',
'graphql_endpoint' => 'http://localhost:6776/graphql/',
'service_identifier' => 'my-php-service',
'service_version' => '1.0.0',
]);
}
// Your application code continues below...
Using a .env File
Most PHP frameworks support .env files natively. For standalone PHP, use vlucas/phpdotenv:
composer require vlucas/phpdotenv
<?php
require_once __DIR__ . '/vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->safeLoad();
if ($_ENV['APP_DEBUG'] ?? false) {
SetupInterceptors::init([
'api_key' => 'sf-veritas-local',
'graphql_endpoint' => 'http://localhost:6776/graphql/',
'service_identifier' => 'my-php-service',
]);
}
# .env
APP_DEBUG=true
Add .env to your .gitignore to avoid committing development settings. Create a .env.example file for reference:
# .env.example
APP_DEBUG=true
Configuration Options
| Option | Type | Required | Description |
|---|---|---|---|
api_key | string | Yes | Use "sf-veritas-local" for local development |
graphql_endpoint | string | Yes | URL of the local collector (default port 6776) |
service_identifier | string | Yes | Unique name for your service |
service_version | string | No | Version of your service |
service_display_name | string | No | Display name in the UI |
git_sha | string | No | Git commit SHA (auto-detected if not set) |
git_org | string | No | Git organization (auto-detected if not set) |
git_repo | string | No | Git repository name (auto-detected if not set) |
domains_to_not_propagate_headers_to | array | No | Domains to skip header propagation |
enable_log_capture | bool | No | Enable Monolog log capture (default: true) |
enable_exception_capture | bool | No | Enable exception capture (default: true) |
enable_print_capture | bool | No | Enable echo/print capture (default: true) |
debug | bool | No | Enable debug logging (default: false) |
What Gets Captured Automatically
Once SetupInterceptors::init() is called, these are captured with zero additional code:
- Logs — All Monolog log entries (info, warning, error, etc.)
- Print statements — All
echo,print, andvar_dumpoutput - Exceptions — Uncaught exceptions with full stack traces
- Inbound HTTP requests — Via framework middleware (Laravel/Symfony)
- Outbound HTTP requests via the framework's native HTTP client — Laravel's
Httpfacade and Symfony's DI-injectedHttpClientInterface. See the HTTP Client Instrumentation section — other HTTP mechanisms (raw Guzzle, cURL,file_get_contents, third-party SDKs) require explicit wiring per client.
PHP has no global HTTP hook. The SDK auto-instruments only the framework's native HTTP service. Direct new GuzzleHttp\Client(), curl_exec(), file_get_contents(), and Guzzle clients embedded inside third-party SDKs (AWS SDK, Stripe, Twilio, Laravel packages, etc.) are NOT instrumented by default. Each one needs the recipe shown in the HTTP Client Instrumentation section below. If distributed tracing headers are missing in downstream services, this is almost always why.
Framework Integration
Laravel
Laravel integration is automatic via the included Service Provider.
1. Publish the Configuration
php artisan vendor:publish --tag=sfveritas-config
This creates config/sfveritas.php where you can customize settings.
2. Set Environment Variables
Add to your .env file:
# .env
APP_DEBUG=true
SAILFISH_API_KEY=sf-veritas-local
SAILFISH_GRAPHQL_ENDPOINT=http://localhost:6776/graphql/
SAILFISH_SERVICE_IDENTIFIER=my-laravel-app
That's it. The Service Provider automatically:
- Registers the request tracking middleware
- Attaches a Monolog log handler
- Adds Guzzle middleware for outbound HTTP tracing
- Registers the exception reporter
- Initializes all collectors
3. Run Your Application
php artisan serve
Laravel Configuration File
The published config/sfveritas.php supports these options:
| Option | Env Variable | Default | Description |
|---|---|---|---|
api_key | SAILFISH_API_KEY | — | API key for local development |
graphql_endpoint | SAILFISH_GRAPHQL_ENDPOINT | http://localhost:6776/graphql/ | Local collector URL |
service_identifier | SAILFISH_SERVICE_IDENTIFIER | — | Unique service name |
service_version | SAILFISH_SERVICE_VERSION | — | Service version |
sf_debug | SF_DEBUG | false | Enable debug logging |
Symfony
Symfony integration uses a Bundle that registers automatically.
1. Register the Bundle
Add to config/bundles.php:
<?php
return [
// ... other bundles
Sailfish\SfVeritas\Bundles\Symfony\SfVeritasBundle::class => ['dev' => true],
];
Registering the bundle only in the dev environment ensures SF Veritas has zero footprint in production.
2. Set Environment Variables
Add to your .env.local:
# .env.local
SAILFISH_API_KEY=sf-veritas-local
SAILFISH_GRAPHQL_ENDPOINT=http://localhost:6776/graphql/
SAILFISH_SERVICE_IDENTIFIER=my-symfony-app
3. Run Your Application
symfony serve
# or
php -S localhost:8000 -t public/
The Bundle automatically:
- Registers a
kernel.requestevent subscriber for inbound request tracking - Registers a
kernel.exceptionsubscriber for exception capture - Decorates the
http_clientservice for outbound HTTP tracing - Registers a Monolog handler for log capture
- Tracks console commands via
console.commandevents
Standalone PHP
For non-framework PHP applications, call SetupInterceptors::init() directly:
<?php
require_once __DIR__ . '/vendor/autoload.php';
use Sailfish\SfVeritas\SetupInterceptors;
if (getenv('APP_DEBUG') === 'true') {
SetupInterceptors::init([
'api_key' => 'sf-veritas-local',
'graphql_endpoint' => 'http://localhost:6776/graphql/',
'service_identifier' => 'my-php-app',
'service_version' => '1.0.0',
]);
}
// Your application code...
Run with:
APP_DEBUG=true php index.php
APP_DEBUG=true php -S localhost:8000
Slim
<?php
// public/index.php
require __DIR__ . '/../vendor/autoload.php';
use Sailfish\SfVeritas\SetupInterceptors;
use Slim\Factory\AppFactory;
if (getenv('APP_DEBUG') === 'true') {
SetupInterceptors::init([
'api_key' => 'sf-veritas-local',
'graphql_endpoint' => 'http://localhost:6776/graphql/',
'service_identifier' => 'slim-api',
'service_version' => '1.0.0',
]);
}
$app = AppFactory::create();
// ... your routes
$app->run();
CodeIgniter
<?php
// app/Config/Events.php
use Sailfish\SfVeritas\SetupInterceptors;
if (getenv('APP_DEBUG') === 'true') {
SetupInterceptors::init([
'api_key' => 'sf-veritas-local',
'graphql_endpoint' => 'http://localhost:6776/graphql/',
'service_identifier' => 'codeigniter-app',
'service_version' => '1.0.0',
]);
}
CakePHP
<?php
// config/bootstrap.php
use Sailfish\SfVeritas\SetupInterceptors;
if (getenv('APP_DEBUG') === 'true') {
SetupInterceptors::init([
'api_key' => 'sf-veritas-local',
'graphql_endpoint' => 'http://localhost:6776/graphql/',
'service_identifier' => 'cakephp-app',
'service_version' => '1.0.0',
]);
}
Yii
<?php
// web/index.php
require __DIR__ . '/../vendor/autoload.php';
use Sailfish\SfVeritas\SetupInterceptors;
if (getenv('APP_DEBUG') === 'true') {
SetupInterceptors::init([
'api_key' => 'sf-veritas-local',
'graphql_endpoint' => 'http://localhost:6776/graphql/',
'service_identifier' => 'yii-app',
'service_version' => '1.0.0',
]);
}
Function Tracing
Use PHP 8 Attributes to control which functions appear in the Flamechart:
use Sailfish\SfVeritas\Attributes\CaptureSpan;
use Sailfish\SfVeritas\Attributes\SkipTracing;
class OrderService
{
#[CaptureSpan]
public function processOrder(string $orderId): array
{
// This function will be traced in the Flamechart
return $this->doWork($orderId);
}
#[SkipTracing]
private function internalHelper(): void
{
// This function won't be traced
}
}
Attribute Options
#[CaptureSpan(
captureArgs: true, // Capture function arguments
captureReturn: true, // Capture return values
sampleRate: 1.0, // Sampling rate (0.0 to 1.0)
argLimitMb: 1, // Max argument capture size in MB
returnLimitMb: 1, // Max return value capture size in MB
autocaptureChildren: true // Auto-capture all child function calls
)]
public function tracedFunction(): void
{
// ...
}
| Attribute | Type | Default | Description |
|---|---|---|---|
captureArgs | bool | true | Capture function arguments |
captureReturn | bool | true | Capture return value |
sampleRate | float | 1.0 | Sampling rate (0.0 to 1.0) |
argLimitMb | int | 1 | Max argument serialization size in MB |
returnLimitMb | int | 1 | Max return value serialization size in MB |
autocaptureChildren | bool | true | Auto-capture all child function calls |
Manual Span Capture
For fine-grained control, use the FunctionSpanProfiler API directly:
use Sailfish\SfVeritas\Collectors\FunctionSpanProfiler;
$result = FunctionSpanProfiler::capture(
fn() => $this->processOrder($orderId),
'processOrder',
['orderId' => $orderId]
);
HTTP Client Instrumentation
Outbound HTTP tracing requires wiring per HTTP mechanism — PHP provides no global hook that lets us intercept every outbound request automatically. The framework integration (Laravel Service Provider / Symfony Bundle) only covers the framework's native HTTP service. Every other mechanism needs explicit wiring so the X-Sf3-Rid trace header propagates and request telemetry is captured.
Coverage Matrix
| # | HTTP Mechanism | Laravel | Symfony | Standalone |
|---|---|---|---|---|
| 1 | Framework-native HTTP client | ✅ Automatic | ✅ Automatic | N/A |
| 2 | Direct Guzzle (new GuzzleHttp\Client()) | ❌ Manual | ❌ Manual | ❌ Manual |
| 3 | Third-party SDKs with embedded Guzzle (AWS, Stripe, Twilio, etc.) | ❌ Manual | ❌ Manual | ❌ Manual |
| 4 | Symfony HttpClient (manually constructed) | ❌ Manual | ❌ Manual* | ❌ Manual |
| 5 | curl_exec() / curl_multi_exec() | ❌ Manual | ❌ Manual | ❌ Manual |
| 6 | file_get_contents() HTTP | ❌ Manual | ❌ Manual | ❌ Manual |
* Only DI-injected HttpClientInterface is auto-decorated. Clients created with HttpClient::create() are not.
1. Framework-native HTTP client (Laravel & Symfony)
Nothing to do. These are wired automatically by the Service Provider / Bundle.
Laravel — Http facade:
use Illuminate\Support\Facades\Http;
$response = Http::get('https://api.example.com/users'); // ✅ instrumented
$response = Http::post('https://api.example.com/orders', [...]); // ✅ instrumented
The Service Provider registers NetworkRequestCollector::middleware() and OutboundHeaderInjector::middleware() as globalMiddleware on Illuminate\Http\Client\Factory, so every request through Http::* carries the trace header.
Symfony — DI-injected HttpClientInterface:
use Symfony\Contracts\HttpClient\HttpClientInterface;
class MyService
{
public function __construct(private HttpClientInterface $client) {}
public function fetch(): array
{
return $this->client->request('GET', 'https://api.example.com/data')->toArray(); // ✅ instrumented
}
}
The Bundle decorates the http_client service with TracingHttpClient, so every client resolved from the container is instrumented.
- Laravel: calls to
new GuzzleHttp\Client()inside a controller or service — see section 2 below. - Symfony: clients built with
HttpClient::create()directly — see section 4 below. - Either framework: third-party SDKs that carry their own Guzzle/HTTP client — see section 3 below.
2. Direct Guzzle client (all frameworks)
Any code that does new GuzzleHttp\Client(...) bypasses Laravel's Http facade and Symfony's DI container. You must attach Sailfish middleware to the client's handler stack.
Laravel / Symfony / Standalone — same recipe:
use GuzzleHttp\Client;
use Sailfish\SfVeritas\Collectors\SailfishGuzzle;
// Option A: Create a fully instrumented client in one call (easiest)
$client = SailfishGuzzle::createClient(['base_uri' => 'https://api.example.com']);
// Option B: Build the handler stack manually (for custom middleware chains)
$client = new Client([
'handler' => SailfishGuzzle::createStack(),
'base_uri' => 'https://api.example.com',
]);
// Option C: Wrap an existing client (returns a new Client; original is not mutated)
$existingClient = new Client(['base_uri' => 'https://api.example.com']);
$client = SailfishGuzzle::wrapClient($existingClient);
$response = $client->get('/users'); // ✅ instrumented
Laravel tip — bind an instrumented Guzzle client in the service container so any service resolving GuzzleHttp\Client receives an instrumented instance:
// app/Providers/AppServiceProvider.php
use GuzzleHttp\Client;
use Sailfish\SfVeritas\Collectors\SailfishGuzzle;
public function register(): void
{
$this->app->bind(Client::class, fn () => SailfishGuzzle::createClient());
}
Symfony tip — register an instrumented Guzzle client as a service:
# config/services.yaml
services:
GuzzleHttp\Client:
factory: ['Sailfish\SfVeritas\Collectors\SailfishGuzzle', 'createClient']
arguments: [[]]
3. Third-party SDKs with embedded Guzzle
SDKs like AWS SDK, Stripe, Twilio, GitHub (knplabs/github-api), Mailgun, many Laravel packages, etc. construct their own Guzzle client internally. These are never touched by Laravel's global middleware or Symfony's DI decoration — you must pass an instrumented Guzzle client/handler into each SDK.
Most SDKs expose a constructor option for this. The three common patterns:
Pattern A — SDK accepts a Guzzle Client:
use Sailfish\SfVeritas\Collectors\SailfishGuzzle;
// Example: Stripe
\Stripe\Stripe::setHttpClient(
new \Stripe\HttpClient\GuzzleClient(SailfishGuzzle::createClient())
);
Pattern B — SDK accepts a Guzzle HandlerStack:
use Aws\S3\S3Client;
use Sailfish\SfVeritas\Collectors\SailfishGuzzle;
$s3 = new S3Client([
'region' => 'us-east-1',
'version' => 'latest',
'http_handler' => \Aws\default_http_handler(),
'handler' => SailfishGuzzle::createStack(), // AWS SDK merges middleware from this stack
]);
For the AWS SDK specifically, you can also add the middleware directly to an existing client:
use Aws\S3\S3Client;
use Sailfish\SfVeritas\Collectors\OutboundHeaderInjector;
use Sailfish\SfVeritas\Collectors\NetworkRequestCollector;
$s3 = new S3Client([/* ... */]);
$s3->getHandlerList()->appendBuild(OutboundHeaderInjector::middleware(), 'sfveritas_headers');
$s3->getHandlerList()->appendBuild(NetworkRequestCollector::middleware(), 'sfveritas_capture');
Pattern C — SDK accepts only a handler callable (no Guzzle at all):
Construct a Guzzle handler stack and pass its handler:
use GuzzleHttp\HandlerStack;
use Sailfish\SfVeritas\Collectors\SailfishGuzzle;
$stack = SailfishGuzzle::createStack();
$handler = $stack; // HandlerStack is itself callable
$sdk = new SomeSdk(['handler' => $handler]);
Check the SDK's client constructor. Most Guzzle-based PHP SDKs accept one of:
'client', 'http_client', 'guzzle', 'handler', 'http_handler', or a setHttpClient() method.
If you cannot inject a client, the SDK is likely using raw cURL — in which case there is nothing to do at the SDK layer; headers will not propagate through it.
4. Symfony HttpClient (manually constructed)
If you build a Symfony HttpClient outside the DI container (e.g., HttpClient::create() in a script), wrap it with SailfishHttpClient:
use Symfony\Component\HttpClient\HttpClient;
use Sailfish\SfVeritas\Collectors\SailfishHttpClient;
$client = new SailfishHttpClient(HttpClient::create());
$response = $client->request('GET', 'https://api.example.com/users'); // ✅ instrumented
This works identically in Laravel, Symfony (for manually-built clients), and Standalone PHP.
5. curl_exec() / curl_multi_exec()
There is no way to monkey-patch curl_exec() at the PHP level. Every curl_exec() call site must be rewritten to CurlInterceptor::exec().
Single-handle:
use Sailfish\SfVeritas\Collectors\CurlInterceptor;
$ch = curl_init('https://api.example.com/users');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']); // set your headers BEFORE exec
$result = CurlInterceptor::exec($ch); // ✅ instrumented (drop-in replacement for curl_exec)
curl_close($ch);
CurlInterceptor::exec()cURL does not expose previously-set headers, so CurlInterceptor cannot read and merge them. Set CURLOPT_HTTPHEADER before the call; CurlInterceptor will append the trace headers to whatever you set.
Multi-handle:
use Sailfish\SfVeritas\Collectors\CurlInterceptor;
$mh = curl_multi_init();
$handles = [];
foreach ($urls as $url) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_multi_add_handle($mh, $ch);
$handles[] = $ch;
}
CurlInterceptor::injectHeadersMulti($mh, $handles); // inject trace headers into all handles
$startTime = microtime(true);
do {
$status = curl_multi_exec($mh, $active);
curl_multi_select($mh);
} while ($active && $status === CURLM_OK);
CurlInterceptor::captureMultiResults($mh, $handles, $startTime); // capture telemetry
foreach ($handles as $ch) {
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
}
curl_multi_close($mh);
grep -rn 'curl_exec\|curl_multi_exec' --include='*.php' src/
Each hit is a rewrite. If rewriting is impractical in legacy code, those requests will not carry trace headers — distributed tracing will stop at the PHP boundary.
6. file_get_contents() HTTP
For HTTP URLs fetched via file_get_contents('https://...'), use HttpStreamInterceptor.
Step 1 — enable the interceptor once at startup (already done by the framework Service Provider / Bundle when SF_INTERCEPT_FILE_GET_CONTENTS=true). For standalone:
# .env
SF_INTERCEPT_FILE_GET_CONTENTS=true
Or programmatically:
use Sailfish\SfVeritas\Collectors\HttpStreamInterceptor;
HttpStreamInterceptor::enable();
Step 2 — replace every file_get_contents() HTTP call site with the wrapper:
use Sailfish\SfVeritas\Collectors\HttpStreamInterceptor;
$data = HttpStreamInterceptor::fileGetContents('https://api.example.com/data'); // ✅ instrumented
// The wrapper is a drop-in replacement: non-HTTP URLs (file://, /local/path) pass through unchanged.
$config = HttpStreamInterceptor::fileGetContents(__DIR__ . '/config.json'); // falls through to file_get_contents()
PHP does not let us replace the http:// and https:// stream wrappers globally without disabling the built-in ones, which is too invasive. Every call site must be rewritten. Finding them:
grep -rn "file_get_contents\\s*(\\s*['\"]https\\?:" --include='*.php' src/
Skipping Specific Domains
To stop the SDK from adding the X-Sf3-Rid header to specific domains (for example, external APIs that reject unknown headers under strict CORS), pass them via domains_to_not_propagate_headers_to:
SetupInterceptors::init([
'api_key' => 'sf-veritas-local',
'graphql_endpoint' => 'http://localhost:6776/graphql/',
'service_identifier' => 'my-service',
'domains_to_not_propagate_headers_to' => [
'api.stripe.com',
'*.amazonaws.com',
'api.twilio.com',
],
]);
Wildcards (*, ?) and subdomain suffix matching are supported. This list applies to all mechanisms above (Guzzle middleware, Symfony decorator, cURL interceptor, stream interceptor).
Installation Checklist
Before declaring instrumentation "done," audit your codebase for each mechanism:
- ☐ Framework HTTP client (
Http::*in Laravel / DIHttpClientInterfacein Symfony) — automatic, nothing to do. - ☐
grep -rn 'new GuzzleHttp\\\\Client\\|new Client(' --include='*.php' src/→ each hit needsSailfishGuzzle::createClient()/SailfishGuzzle::wrapClient()or the DI binding above. - ☐ List every third-party SDK that makes HTTP calls (AWS, Stripe, Twilio, etc.) → pass an instrumented client/handler to each.
- ☐
grep -rn 'HttpClient::create' --include='*.php' src/→ wrap each withnew SailfishHttpClient(...). - ☐
grep -rn 'curl_exec\\|curl_multi_exec' --include='*.php' src/→ rewrite each toCurlInterceptor::exec()/CurlInterceptor::injectHeadersMulti(). - ☐
grep -rn "file_get_contents\\s*(\\s*['\\\"]https\\?:" --include='*.php' src/→ rewrite each toHttpStreamInterceptor::fileGetContents()and setSF_INTERCEPT_FILE_GET_CONTENTS=true.
Docker Compose
For local development with Docker Compose:
FROM php:8.2-cli
WORKDIR /app
# Install extensions
RUN docker-php-ext-install pcntl
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader
COPY . .
ENV APP_DEBUG=true
CMD ["php", "artisan", "serve", "--host=0.0.0.0", "--port=8000"]
# docker-compose.yml
services:
api:
build: .
ports:
- "8000:8000"
environment:
- APP_DEBUG=true
- SAILFISH_API_KEY=sf-veritas-local
- SAILFISH_GRAPHQL_ENDPOINT=http://host.docker.internal:6776/graphql/
- SAILFISH_SERVICE_IDENTIFIER=my-php-service
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.
Environment Variables
| Variable | Default | Description |
|---|---|---|
SAILFISH_API_KEY | — | API key (sf-veritas-local for local dev) |
SAILFISH_GRAPHQL_ENDPOINT | http://localhost:6776/graphql/ | Local collector URL |
SAILFISH_SERVICE_IDENTIFIER | — | Unique service name |
SAILFISH_SERVICE_VERSION | — | Service version string |
SAILFISH_SERVICE_DISPLAY_NAME | — | Display name in the UI |
SF_DEBUG | false | Enable debug logging |
SF_ENABLE_FUNCTION_SPANS | true | Enable function span capture |
SF_FUNCSPAN_CAPTURE_ARGUMENTS | true | Capture function arguments |
SF_FUNCSPAN_CAPTURE_RETURN_VALUE | true | 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_SAMPLING_RATE | 1.0 | Function span sampling rate (0.0 to 1.0) |
SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONS | true | Auto-capture all child function calls |
SF_ENABLE_AUTO_PROFILER | false | Enable automatic function profiling |
SF_AUTO_PROFILER_PATHS | — | Comma-separated paths to auto-profile |
SF_AUTO_PROFILER_EXCLUDE_PATHS | vendor/,storage/,cache/ | Comma-separated paths to exclude |
SF_AUTO_PROFILER_MAX_DEPTH | 10 | Max nesting depth for auto-profiler |
SF_EXCEPTION_CAPTURE_LOCALS | false | Capture local variables in exception frames |
SF_INTERCEPT_FILE_GET_CONTENTS | false | Intercept file_get_contents() HTTP calls |
SF_NETWORKHOP_CAPTURE_ENABLED | true | Enable inbound/outbound HTTP tracing |
SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS | false | Capture HTTP request headers |
SF_NETWORKHOP_CAPTURE_REQUEST_BODY | false | Capture HTTP request bodies |
SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS | false | Capture HTTP response headers |
SF_NETWORKHOP_CAPTURE_RESPONSE_BODY | false | Capture HTTP response bodies |
SF_DISABLE_BATCHING | false | Disable telemetry batching |
PRINT_CONFIGURATION_STATUSES | false | Print config status on startup |
Configuration File
Create a .sailfish file in your project root for per-file and per-function span configuration:
{
"files": {
"*.php": {
"capture_arguments": true,
"capture_return_value": true,
"sample_rate": 1.0
},
"*Test.php": {
"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
}
}
}
Verifying the Setup
- Start your application with debug mode enabled:
- Laravel:
php artisan serve(withAPP_DEBUG=truein.env) - Symfony:
symfony serve(with bundle registered indev) - Standalone:
APP_DEBUG=true php -S localhost:8000
- Laravel:
- In VS Code, open the Command Palette (
Ctrl+Shift+P/Cmd+Shift+P) - Run
SF Veritas: Show Console Logs - Trigger some activity in your application
- You should see logs appearing in the Console panel
Debug Mode (for installation verification only)
Enable debug output to verify instrumentation is working:
SF_DEBUG=true php artisan serve
You'll see Sailfish initialization messages in your terminal output.
Troubleshooting
No logs appearing
- Check the desktop app: Ensure the SF Veritas Desktop App is running
- Verify the endpoint: Ensure
graphql_endpointmatches your server port - Check debug mode: Ensure
APP_DEBUG=trueis set - Check terminal output: Look for Sailfish initialization messages
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
Import errors
- Ensure
sailfish/sf-veritasis installed:composer show sailfish/sf-veritas - Check you're using PHP 8.1+:
php -v - Verify the Sailfish repository is in your
composer.json - Run
composer dump-autoloadto regenerate the autoloader
Multi-Service Setup
When running multiple PHP services locally, give each a unique service_identifier:
// user-service/bootstrap.php
SetupInterceptors::init([
'api_key' => 'sf-veritas-local',
'graphql_endpoint' => 'http://localhost:6776/graphql/',
'service_identifier' => 'user-service',
'service_version' => '1.0.0',
]);
// order-service/bootstrap.php
SetupInterceptors::init([
'api_key' => 'sf-veritas-local',
'graphql_endpoint' => 'http://localhost:6776/graphql/',
'service_identifier' => 'order-service',
'service_version' => '1.0.0',
]);
Use the service filter in the Console to switch between services.