saloonphp / saloon

🤠 Build beautiful API integrations and SDKs with Saloon
https://docs.saloon.dev
MIT License
2.09k stars 107 forks source link

API logs table #345

Closed sauloonze closed 10 months ago

sauloonze commented 11 months ago

Hello Sam and Team,

Great work on the architecture and the documentation effort. I'm working on adding an 'api_log' table to track the API lifecycle.

The table structure is as follows:

        $table->string('url')->nullable();
        $table->text('method')->nullable();
        $table->integer('code')->nullable();
        $table->text('status')->nullable();
        $table->text('headers')->nullable();
        $table->text('request')->nullable();
        $table->text('response')->nullable();
        $table->float('duration')->nullable();
        $table->timestamp('finished_at')->nullable();
        $table->timestamp('failed_at')->nullable();

I attempted to implement this using debugRequest and debugResponse, but it seems that's not quite right. Would it be better to use middleware or use the pipeline?

Any guidance you could provide on the best approach would be much appreciated.

Thanks for the amazing work!

Sammyjo20 commented 11 months ago

Hey @sauloonze thank you for your kind words, it means a lot :)

This is awesome because it's actually something that @craigpotter and I have been working on ourselves - we're working on our own official Saloon request logger for Laravel! 😉 Keep it quiet though 🤫

I'll share a little bit of how we were going to do it which might help you. I won't take the credit for it, this is all Craig's idea.

I assume you're using Laravel based on the migration structure. Assuming you are and you have the saloonphp/laravel-plugin installed, you can actually listen out for two specific events that will get fired whenever a Saloon request is sent. You can then write the request/response into your database table. I recommend listening to the SentSaloonRequest event.

Firstly, you want to create an event listener.

After that, in your EventServiceProvider.php you should add the following code:

<?php

use Saloon\Laravel\Events\SentSaloonRequest;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event to listener mappings for the application.
     *
     * @var array<class-string, array<int, class-string>>
     */
    protected $listen = [
        SentSaloonRequest::class => [
            YourListener::class,
        ],
    ];
}

After that, in your listener you'll have access to the following properties:

$event->pendingRequest
$event->response

The PendingRequest will return everything that was sent to the API including headers, body, URL, and method. It will all be combined.

The Response class contains the response from the API.

Looking at your table - it all looks pretty good! Saloon doesn't actually keep track of duration but I'm going to absolutely write this down because that could be really handy. You could probably work out duration by also hooking onto the SendingSaloonRequest also provided by Saloon and then work out the difference.

Hope this helps and I'll let Craig chime in if he wants to add anything extra

craigpotter commented 11 months ago

I think @Sammyjo20 pretty much summed it up, I don't really have anything to add apart from Sam is far too kind giving me all the credit

sauloonze commented 11 months ago

Thanks for the quick reply and great work, Craigpotter and Sammyjo20!

I'm looking forward to trying out SentSaloonRequest. Right now, I'm working on the debug mode for this feature. Is there a way to add a unique identifier to the request for better logging, maybe through models? Any tips would be really helpful!


        $this->debugRequest(
            function (PendingRequest $pendingRequest, RequestInterface $psrRequest) {
                echo '[API]['.$pendingRequest->getUrl().'] Requesting... ' . PHP_EOL;

                // hide secrets from $pendingRequest->body()

                ApiLogsModel::create([
                    'url' => $pendingRequest->getUrl(),
                    'method' => $pendingRequest->getMethod(),
                    'request_body' => $pendingRequest->body(),
                    'request_query' => $pendingRequest->query()->all(),
                ]);
            }
        );
        $this->debugResponse(function (Response $response) {
            $request = $response->getPendingRequest();
            ApiLogsModel::where('url', $request->getUrl())->update([
                'response_body' => strlen($response->body()),
                'response_code' => $response->status(),
                'finished_at' => now(),
            ]);
            echo '[API]['.$request->getUrl().'] Response Done! - '.strlen($response->body()).' bytes' . PHP_EOL;
        });
Sammyjo20 commented 11 months ago

Hey @sauloonze I apologise for the late reply.

You could make use of Saloon's config to attach a temporary ID to your requests. This won't be sent to the API.

$this->debugRequest(
    function (PendingRequest $pendingRequest, RequestInterface $psrRequest) {
        $id = Str::random();

        $pendingRequest->config()->add('internal-log-id', $id);

        // ...
    }
);

Then on the response, you can find that request ID:

$this->debugResponse(function (Response $response) {
    $pendingRequest = $response->getPendingRequest();

    $id = $pendingRequest->config()->get('internal-log-id');

     // ....
});
sauloonze commented 10 months ago

Thanks @Sammyjo20,

With your help, I completed my task successfully! Here is the piece of code related to the api logs table.

public function __construct()
{
    $this->setUpLogging();
    $this->authenticateWithCachedToken();
}

protected function setUpLogging(): void
{
    $this->debugRequest([$this, 'logRequest']);
    $this->debugResponse([$this, 'logResponse']);
}

public function logRequest(PendingRequest $pendingRequest): void
{
    Log::info('[API]['.$pendingRequest->getUrl().'] Requesting...');

    $apiLog =  ApiLogsModel::start($pendingRequest);

    $pendingRequest->config()->add('internal-log-id', $apiLog->id);
}

public function logResponse(Response $response): void
{
    $pendingRequest = $response->getPendingRequest();

    $id = $pendingRequest->config()->get('internal-log-id');

    $log = ApiLogsModel::where('id', $id)->first();

    $log->finish($response);

    Log::info('[API]['.$pendingRequest->getUrl().'] Response Done... '.strlen($response->body()).' bytes');
}
craigpotter commented 10 months ago

Looks good. If you want a suggestion, I would change:

$log = ApiLogsModel::where('id', $id)->first();

to

$log = ApiLogsModel::where('id', $id)->sole();

If for some reason the ApiLogsModel isn't saved or has been edited, you will get errors on the response as you will be trying to access finish($response) on null