-
Notifications
You must be signed in to change notification settings - Fork 28
[Request] Docker-friendly logging #126
Comments
You can, but not directly through Laravel's $monolog = Log::getMonolog();
// Always add the stderr output for errors over WARNING level.
$monolog->pushHandler(
new \Monolog\Handler\StreamHandler('php://stderr', \Monolog\Logger::WARNING)
);
// Conditionally add stdout debug.
if (config('app.debug')) {
$monolog->pushHandler(
new \Monolog\Handler\StreamHandler('php://stdout', \Monolog\Logger::DEBUG)
);
} Something along those lines! EDIT: I understand that the |
didn't know where to put the sample above... In bootstrap/app.php: $app->configureMonologUsing(function($monolog) {
// Always add the stderr output for errors over WARNING level.
$monolog->pushHandler(
new \Monolog\Handler\StreamHandler('php://stderr', \Monolog\Logger::WARNING)
);
}); |
For Lumen 5.4.*: bootstrap/app.php
|
A solution with no code, only configuration for Laravel and Apache. .env
apache2 site conf
I think you could do the same with nginx. |
thanks @YoannLeViavant - this also works very well with docker/php-fpm |
New logging should be able to handle this. |
I've added an example to the default logging configuration file of how to do this. |
@taylorotwell Is there any handlers for us to route logs to STDOUT? (I can see StreamHandler is for STDERR messages) |
@kisanme The streamhandler is for streams, it does not matter where to. You can use Also see https://laravel.com/docs/5.6/logging#creating-channels-via-factories . |
Figured that out @mfn
|
Surely this should be included out the box? |
In recent versions (5.6+) the default
|
if you are using docker CMD:
apache log had been outputed to stdout, you only need to change laravel config:
|
In regards to log levels, and using @kisanme 's solution above, do not do
instead you must do
|
This is all great for how the logs are sent to the logging channel, i.e. which stream. In our projects we are taking it a little further. We have:
The formatting uses the standard monolog JSON formatter. That gives us a ready-made data structure that we assume (hope!) will follow some well-known conventions that other systems use. If there are better structures that more easily map to teh ELK stack, then this can be tweaked, along with the processors. The finally the additional metadata is provided by a number of processors, added to the logging object through taps. We are collecting a number of standard and custom taps together here, and applying them to a lot of containerised Laravel and Lumen applications: https://github.com/consilience/laravel-extended-logging We will refine those processors over time, but hopefully it is of some interest or use to others in the meantime. It's still in alpha today (no releases) so take it or leave, but I fully intend to extend it further. |
i get Call to undefined method Laravel\Lumen\Application::configureMonologUsing() |
@kareemashraf that issue is long-standing, see https://laracasts.com/discuss/channels/lumen/configure-monolog-handler-for-lumen for some tips. I would suggest you find a workaround rather than extending the application. A stream handler to |
The
Point 2 seems to be the most problematic. The sending of scheduled commands' output to It did occur to me that one could run the scheduler in its own container with the primary command being Can anyone suggest a way to get scheduled commands' output into (PS. I'm aware of the task output option |
I've seen crazy things in docker containers, like processed being redirected to write to |
@Harrisonbro To get the real output of the commands run in the schedule use: A beauty error remains: because of the missing "log processor", the output is completely naked. Would be nice to have the option to pipe the command output through the frameworks logging layer. This way the output could be formatted like the regular logs. |
I've gotten around this limitation by not using commands in my scheduled tasks but Jobs instead: $schedule->job(new TokenDailyRotation)
->timezone('America/Los_Angeles')
->at('03:00'); Which does not have the restriction of writing to /dev/null so log messages like this show up just fine (we configured logging write to stdout and then push them up to cloudwatch): Log::error('could not update token due to error:' . $e->getMessage(), [
'thing' => $thing->name ?? 'unknown',
'job' => get_class(),
]); We find it helpful to include the Job classname in the logs for filtering a bunch of logs that get spewed out at the same scheduled time each day. It also promotes thinking about chaining sub-tasks for looped items as further async jobs by dispatching from the root Job like this: UpdateToken::dispatch(get_class($thing), $thing->id); Also, I find it very useful to write tests for these kinds of things to make sure that you catch issues like DST changes and whatnot. I have a trait to find a job from the schedule namespace Tests\Unit\Console\Scheduler;
use Illuminate\Console\Scheduling\CallbackEvent;
use Illuminate\Console\Scheduling\Event;
trait ScheduledJobEvents
{
/**
* Get the event matching the given command signature from the scheduler
*
* @param string $jobSignature
* @return CallbackEvent|null
*/
private function getJobEvent(string $jobSignature): CallbackEvent
{
$event = collect($this->schedule->events())->first(function (Event $event) use ($jobSignature) {
return stripos($event->description , $jobSignature);
});
if (!$event) {
$this->fail('Event for "' . $jobSignature . '" not found in ' . $this->schedule->events());
}
return $event;
}
} and another trait for event helpers: namespace Tests\Unit\Console\Scheduler;
use Carbon\Carbon;
use Cron\CronExpression;
use Illuminate\Console\Scheduling\Event;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Support\Facades\Log;
trait ScheduledEvents
{
/**
* @var Schedule|null
*/
protected Schedule|null $schedule = null;
/**
* Asserts that the given event will execute on the given test date
*
* @param Event $event
* @param Carbon $testDate
*/
protected function assertWillRun(Event $event, Carbon $testDate, string $message = '')
{
if ($message == '') {
$message = 'Task should run on ' . $testDate->toIso8601String();
}
$this->assertTrue($this->willEventRun($event, $testDate), $message);
}
/**
* Check if the given event will run on the given test date
*
* @param Event $event
* @param Carbon $testDate
* @return bool
*/
protected function willEventRun(Event $event, Carbon $testDate)
{
Carbon::setTestNow($testDate);
// Run the when() & skip() filters
$filtersPass = $event->filtersPass($this->app);
Log::debug('checking the when() and skip() filters', ['result' => $filtersPass]);
// Test that the Cron expression passes
$datePasses = $this->isEventDue($event);
Log::debug('checking the cron expression', ['result' => $datePasses]);
return $filtersPass && $datePasses;
}
/**
* Determine if the Cron expression passes.
*
* @param Event $event
* @return bool
*/
protected function isEventDue(Event $event): bool
{
$date = Carbon::now();
if ($event->timezone) {
$date->setTimezone($event->timezone);
}
$expression = new CronExpression($event->expression);
return $expression->isDue($date->toDateTimeString());
}
/**
* Asserts that the given event will NOT execute on the given test date
*
* @param Event $event
* @param Carbon $testDate
*/
protected function assertWillNotRun(Event $event, Carbon $testDate, string $message = '')
{
if ($message == '') {
$message = 'Task should run not on ' . $testDate->toIso8601String();
}
$this->assertFalse($this->willEventRun($event, $testDate), $message);
}
} a common TestCase for scheduled events that handles schedule setup/teardown namespace Tests\Unit\Console\Scheduler;
use Illuminate\Console\Scheduling\Schedule;
use Tests\TestCase;
class ScheduleTestCase extends TestCase
{
use ScheduledEvents;
/**
* Make sure that the test is set up
*/
public function setUp(): void
{
parent::setUp();
if (!$this->schedule) {
$this->refreshSchedule();
}
}
/**
* Refresh the schedule instance.
*
* @return void
*/
protected function refreshSchedule()
{
$this->schedule = $this->app->make(Schedule::class);
}
/**
* Make sure we clean up after the test
*/
public function tearDown(): void
{
$this->schedule = null;
parent::tearDown();
}
} and finally a test case that uses that stuff, note that I test other kinds of events as well which is why the class TokenDailyRotationTest extends ScheduleTestCase
{
use ScheduledJobEvents;
/**
* Ensure that it's 3AM that we are scheduled for
*/
public function testScheduleIsMorning() {
$event = $this->getJobEvent('TokenDailyRotation');
$testDate = Carbon::now('America/Los_Angeles')
->startOfDay()
->addHours(15); // 3 PM
$this->assertWillNotRun($event, $testDate);
}
} Good luck |
Some updates on the way I managed around this problem. Each task or role is run in a separate container:
Each of this variants needs some adjustments to get consistent logging from all of them. One key point is the monolog-docker-handler that allows to write the logs directly to The tricky part is to get the Unfortunately I haven't found a good way to detect if the command is run as schedule and therefor should log or use the default output. My workaround this far is to detect if the command runs inside the To the implementation. At this point the code is part of our proprietary Docker Support Package, as it is highly tailored to our setup. But I plan a more general variant of this to be published in the future. So take this as an inspiration. The trait used on all custom commands and the <?php
namespace byteit\DockerSupport\Console;
use byteit\DockerSupport\Facades\Docker;
use Illuminate\Console\Concerns\InteractsWithIO;
use Illuminate\Support\Facades\Log;
trait LogsOutput
{
use InteractsWithIO;
/**
* Write a string as standard output.
*
* @param string $string
* @param string|null $style
* @param int|string|null $verbosity
*
* @return void
*/
public function line(
$string,
$style = null,
$verbosity = null
) {
// If run as schedule in container, redirect the output to the logger
if (Docker::isSchedule()) {
$style = $style ?: 'info';
$message = method_exists($this, 'getName') ? sprintf('[%s] %s', $this->getName(), $string) : $string;
Log::log($style, $message);
} else {
parent::line($string, $style, $verbosity);
}
}
} The <?php
namespace byteit\DockerSupport\Console\Command;
use byteit\DockerSupport\Facades\Docker;
use Illuminate\Contracts\Queue\Job;
use Illuminate\Support\Facades\Log;
class WorkCommand extends \Illuminate\Queue\Console\WorkCommand
{
protected function writeStatus(Job $job, $status, $type)
{
if(Docker::isQueue()){
$type = in_array($type, ['info', 'error', 'debug'], true) ? $type : 'info';
Log::log($type, sprintf(
"[%s] %s %s",
$job->getJobId(),
str_pad("{$status}:", 11), $job->resolveName()
));
}
else{
parent::writeStatus($job, $status, $type);
}
}
} In your class ServiceProvider
{
//...
/**
* Register any application services.
*
* @return void
*/
public function register(): void
{
$this->app->extend('command.queue.work', function ($command, $app) {
return new WorkCommand($app['queue.worker'], $app['cache.store']);
});
$this->app->extend(ScheduleRunCommand::class, function () {
return new \byteit\DockerSupport\Console\Command\ScheduleRunCommand();
});
}
} |
@captnCC Thank you, this was very helpful indeed! Just two things to note for future readers:
|
Docker expects all logs to be sent to
stdout
andstderr
and Laravel doesn't seem to have the ability to separate logs in such a way, but requires all logs go to either rather than the ability to split them up. It'd be great if we were able to senderror
logs tostderr
and the rest of the logs tostdout
.The text was updated successfully, but these errors were encountered: