Skip to main content

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:

Add to your composer.json
{
"repositories": [
{
"type": "composer",
"url": "https://satis.sailfishqa.com/repo"
}
]
}

2. Install the Package

composer require sailfish/sf-veritas
Requirements
  • PHP 8.1 or higher
  • ext-curl extension (usually enabled by default)
  • ext-json extension (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:

Activating in development

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.

Run in development mode for local/non-Enterprise
<?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
tip

Add .env to your .gitignore to avoid committing development settings. Create a .env.example file for reference:

# .env.example
APP_DEBUG=true

Configuration Options

OptionTypeRequiredDescription
api_keystringYesUse "sf-veritas-local" for local development
graphql_endpointstringYesURL of the local collector (default port 6776)
service_identifierstringYesUnique name for your service
service_versionstringNoVersion of your service
service_display_namestringNoDisplay name in the UI
git_shastringNoGit commit SHA (auto-detected if not set)
git_orgstringNoGit organization (auto-detected if not set)
git_repostringNoGit repository name (auto-detected if not set)
domains_to_not_propagate_headers_toarrayNoDomains to skip header propagation
enable_log_captureboolNoEnable Monolog log capture (default: true)
enable_exception_captureboolNoEnable exception capture (default: true)
enable_print_captureboolNoEnable echo/print capture (default: true)
debugboolNoEnable 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, and var_dump output
  • 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 Http facade and Symfony's DI-injected HttpClientInterface. See the HTTP Client Instrumentation section — other HTTP mechanisms (raw Guzzle, cURL, file_get_contents, third-party SDKs) require explicit wiring per client.
Outbound HTTP is NOT fully automatic

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:

OptionEnv VariableDefaultDescription
api_keySAILFISH_API_KEYAPI key for local development
graphql_endpointSAILFISH_GRAPHQL_ENDPOINThttp://localhost:6776/graphql/Local collector URL
service_identifierSAILFISH_SERVICE_IDENTIFIERUnique service name
service_versionSAILFISH_SERVICE_VERSIONService version
sf_debugSF_DEBUGfalseEnable 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],
];
tip

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:

Local environment variables (.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

Start application via syfony or php
symfony serve
# or
php -S localhost:8000 -t public/

The Bundle automatically:

  • Registers a kernel.request event subscriber for inbound request tracking
  • Registers a kernel.exception subscriber for exception capture
  • Decorates the http_client service for outbound HTTP tracing
  • Registers a Monolog handler for log capture
  • Tracks console commands via console.command events

Standalone PHP

For non-framework PHP applications, call SetupInterceptors::init() directly:

How to setup standalone PHP applications
<?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:

Run PHP application
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:

Add attributes to functions so Sailfish can capture function timing
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

Restricting individual function captures by controlling parameters directly
#[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
{
// ...
}
AttributeTypeDefaultDescription
captureArgsbooltrueCapture function arguments
captureReturnbooltrueCapture return value
sampleRatefloat1.0Sampling rate (0.0 to 1.0)
argLimitMbint1Max argument serialization size in MB
returnLimitMbint1Max return value serialization size in MB
autocaptureChildrenbooltrueAuto-capture all child function calls

Manual Span Capture

For fine-grained control, use the FunctionSpanProfiler API directly:

Manual Function Span Capture (highly NOT recommended)
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 MechanismLaravelSymfonyStandalone
1Framework-native HTTP client✅ Automatic✅ AutomaticN/A
2Direct Guzzle (new GuzzleHttp\Client())❌ Manual❌ Manual❌ Manual
3Third-party SDKs with embedded Guzzle (AWS, Stripe, Twilio, etc.)❌ Manual❌ Manual❌ Manual
4Symfony HttpClient (manually constructed)❌ Manual❌ Manual*❌ Manual
5curl_exec() / curl_multi_exec()❌ Manual❌ Manual❌ Manual
6file_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.

Does NOT cover
  • 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]);
How do I find the right option?

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);
Set your headers before calling 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);
Finding every call site
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()
Not a true hook

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:

  1. ☐ Framework HTTP client (Http::* in Laravel / DI HttpClientInterface in Symfony) — automatic, nothing to do.
  2. grep -rn 'new GuzzleHttp\\\\Client\\|new Client(' --include='*.php' src/ → each hit needs SailfishGuzzle::createClient() / SailfishGuzzle::wrapClient() or the DI binding above.
  3. ☐ List every third-party SDK that makes HTTP calls (AWS, Stripe, Twilio, etc.) → pass an instrumented client/handler to each.
  4. grep -rn 'HttpClient::create' --include='*.php' src/ → wrap each with new SailfishHttpClient(...).
  5. grep -rn 'curl_exec\\|curl_multi_exec' --include='*.php' src/ → rewrite each to CurlInterceptor::exec() / CurlInterceptor::injectHeadersMulti().
  6. grep -rn "file_get_contents\\s*(\\s*['\\\"]https\\?:" --include='*.php' src/ → rewrite each to HttpStreamInterceptor::fileGetContents() and set SF_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
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.

Environment Variables

VariableDefaultDescription
SAILFISH_API_KEYAPI key (sf-veritas-local for local dev)
SAILFISH_GRAPHQL_ENDPOINThttp://localhost:6776/graphql/Local collector URL
SAILFISH_SERVICE_IDENTIFIERUnique service name
SAILFISH_SERVICE_VERSIONService version string
SAILFISH_SERVICE_DISPLAY_NAMEDisplay name in the UI
SF_DEBUGfalseEnable debug logging
SF_ENABLE_FUNCTION_SPANStrueEnable function span capture
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_SAMPLING_RATE1.0Function span sampling rate (0.0 to 1.0)
SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONStrueAuto-capture all child function calls
SF_ENABLE_AUTO_PROFILERfalseEnable automatic function profiling
SF_AUTO_PROFILER_PATHSComma-separated paths to auto-profile
SF_AUTO_PROFILER_EXCLUDE_PATHSvendor/,storage/,cache/Comma-separated paths to exclude
SF_AUTO_PROFILER_MAX_DEPTH10Max nesting depth for auto-profiler
SF_EXCEPTION_CAPTURE_LOCALSfalseCapture local variables in exception frames
SF_INTERCEPT_FILE_GET_CONTENTSfalseIntercept file_get_contents() HTTP 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_DISABLE_BATCHINGfalseDisable telemetry batching
PRINT_CONFIGURATION_STATUSESfalsePrint 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

  1. Start your application with debug mode enabled:
    • Laravel: php artisan serve (with APP_DEBUG=true in .env)
    • Symfony: symfony serve (with bundle registered in dev)
    • Standalone: APP_DEBUG=true php -S localhost:8000
  2. In VS Code, open the Command Palette (Ctrl+Shift+P / Cmd+Shift+P)
  3. Run SF Veritas: Show Console Logs
  4. Trigger some activity in your application
  5. 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

  1. Check the desktop app: Ensure the SF Veritas Desktop App is running
  2. Verify the endpoint: Ensure graphql_endpoint matches your server port
  3. Check debug mode: Ensure APP_DEBUG=true is set
  4. Check terminal output: Look for Sailfish initialization messages

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

Import errors

  1. Ensure sailfish/sf-veritas is installed: composer show sailfish/sf-veritas
  2. Check you're using PHP 8.1+: php -v
  3. Verify the Sailfish repository is in your composer.json
  4. Run composer dump-autoload to 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.

Next Steps