Read what the SDK writes to your log

PHP

Hand the SDK a logger, and read the events it writes for each request — the route, the status and the time, never a key or a body.

5 min read

The SDK keeps a journal of what it does: each request and its answer, each repeat, each refusal and failure, the reads of a wait and the webhooks it refused. It writes the journal to the logger you give it, and nowhere without one.

Give the SDK a logger

PHP
use Psr\Log\AbstractLogger;

// Stands in for the PSR-3 logger of your application: it prints each event with
// the route and, for an answer, its status.
$logger = new class extends AbstractLogger {
    public function log($level, string|\Stringable $message, array $context = []): void
    {
        $status = isset($context['status']) ? ' ' . $context['status'] : '';

        echo "{$level} {$message} {$context['method']} {$context['route']}{$status}", PHP_EOL;
    }
};
PHP
use Slen\Sdk\SlenConfig;
use Slen\Sdk\SlenSdk;

$slen = SlenSdk::create(new SlenConfig(
    apiKey: (string) getenv('SLEN_API_KEY'),
    logger: $logger,
));

The parameter logger takes any PSR-3 logger. Each event is a message with a context of its fields. In a Laravel application, SLEN_LOG_CHANNEL names the channel of the journal, and without it the journal goes to the log of your application — Laravel integration.

PHP
use Slen\Sdk\Modules\Operations\Options\ExecuteOptions;

$execution = $slen->operations()->execute(
    'generate_email',
    ['messages' => [['role' => 'user', 'content' => 'Write a short follow-up email.']]],
    new ExecuteOptions(idempotencyKey: 'order-77:email'),
);

The run writes two events, and the logger of the example prints them:

debug slen.request POST /v1/operations/execute
debug slen.response POST /v1/operations/execute 200

A logger that throws does not fail the call it writes about.

A logger for one request

A server that handles many requests at once may want the journal of each one in a logger of its own — a logger that carries the id of the request, say. A built SDK does not change, so it is not given another logger: it makes a copy that writes to one.

SlenSdk::withLogger() returns a new SDK that writes to the logger you pass. The copy sends its requests through the same HTTP client and records its spans with the same recorder. The SDK it was made from keeps writing to its own logger. Build the SDK once, and make a copy for each request that needs one — Laravel integration.

The events

EventLevelWhen
slen.requestdebugBefore each attempt of a request
slen.responsedebugAn answer 2xx, with its status, the time of the call and the id of the request
slen.retrywarningAn attempt that will be repeated, with the pause and the reason: no_answer, rate_limited for a 429, unavailable for a 503
slen.refusedinfoAn answer 4xx that refuses the call: money, a policy, a check of the request, a conflict, an unknown customer
slen.unauthorizederrorA 401, or a 403 about the key itself — insufficient_scope, invalid_api_key, not_a_member: a setting to fix
slen.failederrorA 5xx, an answer that cannot be read, or no answer after every attempt, with the type of the error and the number of attempts
slen.wait.polldebugA read of a wait, with the status it read and the pause before the next one
slen.wait.poll_failedwarningA read of a wait that failed for a passing reason; the wait goes on
slen.webhook.rejectedwarningA webhook delivery whose signature or body was refused, with the reason — Webhooks

The PHP SDK writes one more event: slen.config.timeout_not_applied, a warning, when it found an HTTP client it cannot give its timeouts to — Configuration — and when a call asks for a timeout its HTTP client cannot take. LogMessages holds the names of the events as constants.

The fields of each event, as the context of a record carries them:

JSON
{
  "slen.request": { "method": "POST", "route": "/v1/operations/execute", "attempt": 1 },
  "slen.retry": [
    { "method": "POST", "route": "/v1/operations/execute", "attempt": 1, "delay_ms": 214, "reason": "no_answer", "status": null },
    { "method": "POST", "route": "/v1/operations/execute", "attempt": 2, "delay_ms": 2000, "reason": "rate_limited", "status": 429 },
    { "method": "POST", "route": "/v1/operations/execute", "attempt": 1, "delay_ms": 812, "reason": "unavailable", "status": 503 }
  ],
  "slen.response": { "method": "POST", "route": "/v1/operations/execute", "status": 200, "duration_ms": 1843, "request_id": "0d3c1b9e-5a47-4f0e-9a36-2f6c8d1e7b42" },
  "slen.refused": { "method": "POST", "route": "/v1/customers/{externalUserId}/charge", "status": 402, "error_code": "insufficient_credits", "request_id": "9c2d7e41-6b0a-4f3e-8d15-2a7f9e0c4b63" },
  "slen.failed": { "method": "POST", "route": "/v1/operations/execute", "status": null, "error_code": null, "error_type": "Connection", "request_id": null, "attempts": 3 },
  "slen.wait.poll": { "execution_id": "74438200973594800", "status": "running", "next_poll_ms": 2000 },
  "slen.wait.poll_failed": { "execution_id": "74438200973594800", "failures_in_a_row": 1, "error_type": "RateLimit" },
  "slen.webhook.rejected": { "reason": "no_matching_signature", "age_seconds": 2 }
}
  • route is the template of the path, not the path with its values: /v1/customers/{externalUserId}/charge, never the id of your customer.

  • duration_ms of an answer is the time of the whole call, repeats and pauses included.

  • error_type is the name of the type of the error without its ending — Connection for ConnectionException and ConnectionError — the same word in both SDKs.

  • execution_id is written: it is what you look an execution up by in the console.

What the journal never carries

  • the API key and the Authorization header;

  • the signing secret of your webhooks;

  • the idempotency key — it carries the ids of your own work;

  • the bodies of requests and answers: the input and the output of a flow, its messages and prompts;

  • the body of a webhook delivery;

  • the message of an error the API answered with, which can carry the text a flow wrote — only its code;

  • the values of the path and of the query.

So the journal can go to the log of your application as it is.

Where to go next