Skip to content

[11.x] Introduce storing log channel name on Context #54692

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from

Conversation

cosmastech
Copy link
Contributor

@cosmastech cosmastech commented Feb 19, 2025

The Context facade is nearly perfect. I love that I can carry about contextual information from function to function without having to a pass an instance of a LogInterface or the contextual information as an array.

However, the one missing piece is that I cannot set the channel name via Context. So for instance, I currently have to write code like this:

Context::add(['user_id' => 1234, 'team' => 'Blackhawks']);
Log::withName('user-sync')->info('Preparing team sync.');
// ... other stuff
Log::withName('user-sync')->info('Team sync complete.');

which will produce logs like this (when written to a log file)

[2025-02-19 03:20:11] user-sync.INFO: Preparing team sync.  {"user_id":1234,"team":"Blackhawks"}
[2025-02-19 03:20:11] user-sync.INFO: Team sync complete.  {"user_id":1234,"team":"Blackhawks"}

Obviously, within a single function this isn't too bad, but when the information has multiple calls to other services, it can be unwieldy.

Why would you need to use this?

In a large application, observability tools like DataDog make it possible to query just by the channel name. If your application writes thousands of logs per hour, being able to filter down to only the relevant logs is incredibly helpful. This is like adding additional context, except it is standardized for logging platforms.

A solution

Introducing Context::setName(). We can pass a string here, and now all of our logs with shared context will be under the same name.

Context::setName('user-sync');
Context::add(['user_id' => 1234, 'team' => 'Blackhawks']);
Log::info('Preparing team sync.');
// ... other stuff
Log::info('Team sync complete.');

Limitations

You cannot override the channel name without modifying the Context. So, if I want to change the channel name for just a single log (Log::withName('not-user-sync')->info('Doing something else.')), the name set by the context will override the value specified. I considered some options for this, but didn't really have any clear approach. I thought about maybe storing a reference to channel name on Log\Logger, but not sure how I feel about that.

@cosmastech cosmastech changed the title [11.x] Introduce storing name on Context [11.x] Introduce storing log channel name on Context Feb 19, 2025
@taylorotwell
Copy link
Member

I personally find this a bit confusing. Going to let @timacdonald take a look.

@taylorotwell taylorotwell marked this pull request as draft February 19, 2025 21:50
@timacdonald
Copy link
Member

@cosmastech, I'm not sure I understand the problem you are trying to solve. Could you provide some more real-world code examples or elaborate a little more? I would love to understand.

@cosmastech
Copy link
Contributor Author

Sure! Say we want to log all actions under one channel name. We start with a web request that calls an action, which in turn calls another service.

Here's a code snippet as a for instance
<?php

use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use App\Models\Team;
use App\Models\School;
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class SyncTeamController extends Controller
{
    public function __invoke(School $school, SyncTeamsAction $action)
    {
        Context::add(['school_id' => $school->id]);
        Context::setName('sync-school-teams-request');

        $action->handle($school);

        return response()->noContent();
    }
}


class SyncTeamsAction
{
    public function __construct(private readonly ExternalTeamsFetcher $teamFetcher) {}

    public function handle(School $school)
    {
        Log::debug("Preparing to sync teams for School {$school->name}. Last synced at: " . $school->last_synced_at);

        $teamsToSync = $this->teamFetcher->handle($school->externalId);

        foreach($teamsToSync as $team) {
            // ... imagine logging some additional details here, like when we mark a team inactive, or create a new team.
        }
    }
}


class ExternalTeamsFetcher
{
    public function handle(string $externalSchoolId): array
    {
        Log::debug('Fetching team from external API');

        $response = Http::get('https://example.com/schools/' . $externalSchoolId . '/teams');

        $json = $response->json();
        Log::debug('Rate limit remaining: ' . $response->header('X-RateLimit-Remaining'));

        return $this->mapResponse($json);
    }

    private function mapResponse(array $response): array
    {
        // ...
    }
}

In order to keep all of these logs in a shared context, we could set a channel name at the outset. Then through each step of the process, we can see them grouped together.

Obviously, there are other workarounds for this. (Like adding Context::add('action', 'sync-school-teams-request')) and then modifying the table in DataDog to display extra.action when browsing logs.

@shaedrich
Copy link
Contributor

shaedrich commented Feb 20, 2025

I feel like setName() doesn't cut it. It somewhat works in the … context … of logging, but that contextual information is missing when calling it on the context. withLogChannel() or the like would make that clearer 🤔

@timacdonald
Copy link
Member

@cosmastech, just for my understanding, is this what it could look like without the feature yet implemented?

<?php

use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use App\Models\Team;
use App\Models\School;
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class SyncTeamController extends Controller
{
    public function __invoke(School $school, SyncTeamsAction $action)
    {
        Context::add(['school_id' => $school->id]);

        $action->handle($school);

        return response()->noContent();
    }
}


class SyncTeamsAction
{
    public function __construct(private readonly ExternalTeamsFetcher $teamFetcher) {}

    public function handle(School $school)
    {
        Log::withName('sync-school-teams-request')->debug("Preparing to sync teams for School {$school->name}. Last synced at: " . $school->last_synced_at);

        $teamsToSync = $this->teamFetcher->handle($school->externalId);

        foreach($teamsToSync as $team) {
            // ... imagine logging some additional details here, like when we mark a team inactive, or create a new team.
        }
    }
}


class ExternalTeamsFetcher
{
    public function handle(string $externalSchoolId): array
    {
        Log::withName('sync-school-teams-request')->debug('Fetching team from external API');

        $response = Http::get('https://example.com/schools/' . $externalSchoolId . '/teams');

        $json = $response->json();
        Log::withName('sync-school-teams-request')->debug('Rate limit remaining: ' . $response->header('X-RateLimit-Remaining'));

        return $this->mapResponse($json);
    }

    private function mapResponse(array $response): array
    {
        // ...
    }
}

@cosmastech
Copy link
Contributor Author

@cosmastech, just for my understanding, is this what it could look like without the feature yet implemented?

Yep!

@timacdonald timacdonald self-assigned this Feb 21, 2025
@taylorotwell
Copy link
Member

I think we're going to table this one for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants