Read what the SDK writes to your log
PHPHand 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
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;
}
};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.
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 200A 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
| Event | Level | When |
|---|---|---|
slen.request | debug | Before each attempt of a request |
slen.response | debug | An answer 2xx, with its status, the time of the call and the id of the request |
slen.retry | warning | An attempt that will be repeated, with the pause and the reason: no_answer, rate_limited for a 429, unavailable for a 503 |
slen.refused | info | An answer 4xx that refuses the call: money, a policy, a check of the request, a conflict, an unknown customer |
slen.unauthorized | error | A 401, or a 403 about the key itself — insufficient_scope, invalid_api_key, not_a_member: a setting to fix |
slen.failed | error | A 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.poll | debug | A read of a wait, with the status it read and the pause before the next one |
slen.wait.poll_failed | warning | A read of a wait that failed for a passing reason; the wait goes on |
slen.webhook.rejected | warning | A 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:
{
"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 }
}routeis the template of the path, not the path with its values:/v1/customers/{externalUserId}/charge, never the id of your customer.duration_msof an answer is the time of the whole call, repeats and pauses included.error_typeis the name of the type of the error without its ending —ConnectionforConnectionExceptionandConnectionError— the same word in both SDKs.execution_idis written: it is what you look an execution up by in the console.
What the journal never carries
the API key and the
Authorizationheader;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
Spans — one record of each run, with its money, for your traces.
Retries and timeouts — what
slen.retryreports.Configuration — the other options of the SDK.