Laravel logging with Monolog\Handler\BrowserConsoleHandler - php

How can Laravel 5's logging be changed to Monolog\Handler\BrowserConsoleHandler?
What doesn't work in Laravel 5 but does work in a standalone PHP file:
use Illuminate\Support\Facades\Log;
use Monolog\Handler\BrowserConsoleHandler;
use Monolog\Logger;
// create a log channel
$log = Log::getMonolog();
// $log = new Logger('Testlogger'); //doesn't make any difference
$log->pushHandler(new BrowserConsoleHandler(\Psr\Log\LogLevel::INFO));
// add records to the log
$log->addWarning('Foo');
$log->addError('Bar');
All that happens is that my logs appear in the logfile but don't find their way to the browser. If I try the code in a single PHP file without framework it works, so I assume it's a Laravel problem.
I get it working with Firebug and FirePHP installed and $log->pushHandler(new FirePHPHandler()); instead of BrowserConsoleHandler but this is not a solution since it sends the logs with headers but I already sent some debug-echos when the logger wants to send the headers.
BrowserConsoleHandler on the other hand adds a JavaScript snippet to the end of the site that perfectly fits my needs.
So, did anyone succeed in adding BrowserConsoleHandler to Laravel's logging? How?

After reading plenty of source code and getting xdebug to work I finally figuered it out:
BrowserConsoleHandler sends the script snipped after finishing the php script by register_shutdown_function(). At this time, Laravel already sent the full response to the browser. So the script snipped from BrowseConsoleHandler gets generated but never sent to the browser.
As a workaround you can build your own Middleware (http://laravel.com/docs/5.0/middleware) that invokes the code generation manually and adds it to the response before it gets sent.
Create app/Http/Middleware/LogBrowserConsole.php:
<?php
namespace App\Http\Middleware;
use Illuminate\Contracts\Routing\Middleware;
use Illuminate\Support\Facades\Log;
use Monolog\Handler\BrowserConsoleHandler;
class LogBrowserConsole implements Middleware {
public function handle($request, \Closure $next)
{
// add BrowserConsoleHandler to Laravel's Logger
$log = Log::getMonolog();
$log->pushHandler(new BrowserConsoleHandler(\Psr\Log\LogLevel::INFO));
// invokes all your stuff like it would do without the middleware but with the new logger
$response = $next($request);
// after the request is done we care about the log entries
$handlers = $log->getHandlers();
$scriptSnippet = "";
foreach($handlers as $handler){ // only handle BrowserConsoleHandler
if($handler instanceof BrowserConsoleHandler){
ob_start(); //start output buffer so we can save echo to variable
$handler->send(); // create the scriptSnipped
$scriptSnippet .= ob_get_clean();
}
}
// write scriptSnippet to end of response content
$content = $response->getContent();
$response->setContent($content.$scriptSnippet);
return $response;
}
}
Register the Middleware in app/Http/Kernel.php:
protected $routeMiddleware = [
'log.browserconsole' => 'App\Http\Middleware\LogBrowserConsole'
];
and invoke your Controller with the Middleware in app/Http/routes.php:
Route::get('test', ['middleware' => 'log.browserconsole', 'uses'=>'TestController#test']);
Alternatively, if you want to use the Middleware for every request you can add it to
protected $middleware = [
'App\Http\Middleware\LogBrowserConsole'
];
in app/Http/Kernel.php.
Your Route would look like Route::get('test', 'TestController#test');
Now, your Log::debug() etc. messages get sent to the logfile (the default LogHandler is still available, you just added another) and the script snipped from BrowserConsoleHandler gets built and sent to the browser with all your log items.
Keep in mind to eventually change the log level \Psr\LogLevel::INFO in app/Http/Middleware/LogBrowserConsole to fit your needs.

Related

Laravel 5.3 changing logfiles for specific console commands

There are two noisy console commands in my Laravel 5.3 app that I want to keep logs for but would prefer to have them write to a different log file from the rest of the system.
Currently my app writes logs to a file configured in bootstrap/app.php using $app->configureMonologUsing(function($monolog) { ...
Second prize is writing all console commands to another log file, but ideally just these two.
I tried following these instructions (https://blog.muya.co.ke/configure-custom-logging-in-laravel-5/ and https://laracasts.com/discuss/channels/general-discussion/advance-logging-with-laravel-and-monolog) to reroute all console logs to another file but it did not work and just caused weird issues in the rest of the code.
If this is still the preferred method in 5.3 then I will keep trying, but was wondering if there was newer method or a method to only change the file for those two console commands.
They are two approaches you could take
First, you could use Log::useFiles or Log::useDailyFiles like suggests here.
Log::useDailyFiles(storage_path().'/logs/name-of-log.log');
Log::info([info to log]);
The downside of this approach is that everything will still be log in your default log file because the default Monolog is executed before your code.
Second, to avoid to have everything in your default log, you could overwrite the default logging class. An exemple of this is given here. You could have a specific log file for let's say Log::info() and all the others logs could be written in your default file. The obvious downside of this approach is that it requires more work and code maintenance.
This is possible but first you need to remove existing handlers.
Monolog already has had some logging handlers set, so you need to get rid of those with $monolog->popHandler();. Then using Wistar's suggestion a simple way of adding a new log is with $log->useFiles('/var/log/nginx/ds.console.log', $level='info');.
public function fire (Writer $log)
{
$monolog = $log->getMonolog();
$monolog->popHandler();
$log->useFiles('/var/log/nginx/ds.console.log', $level='info');
$log->useFiles('/var/log/nginx/ds.console.log', $level='error');
...
For multiple handlers
If you have more than one log handler set (if for example you are using Sentry) you may need to pop more than one before the handlers are clear. If you want to keep a handler, you need to loop through all of them and then readd the ones you wanted to keep.
$monolog->popHandler() will throw an exception if you try to pop a non-existant handler so you have to jump through hoops to get it working.
public function fire (Writer $log)
{
$monolog = $log->getMonolog();
$handlers = $monolog->getHandlers();
$numberOfHandlers = count($handlers);
$saveHandlers = [];
for ($idx=0; $idx<$numberOfHandlers; $idx++)
{
$handler = $monolog->popHandler();
if (get_class($handler) !== 'Monolog\Handler\StreamHandler')
{
$saveHandlers[] = $handler;
}
}
foreach ($saveHandlers as $handler)
{
$monolog->pushHandler($handler);
}
$log->useFiles('/var/log/nginx/ds.console.log', $level='info');
$log->useFiles('/var/log/nginx/ds.console.log', $level='error');
...
For more control over the log file, instead of $log->useFiles() you can use something like this:
$logStreamHandler = new \Monolog\Handler\StreamHandler('/var/log/nginx/ds.console.log');
$pid = getmypid();
$logFormat = "%datetime% $pid [%level_name%]: %message%\n";
$formatter = new \Monolog\Formatter\LineFormatter($logFormat, null, true);
$logStreamHandler->setFormatter($formatter);
$monolog->pushHandler($logStreamHandler);

Set up Monolog in Laravel global middleware

In my local environment I want all logs (all flags) to go the browser's console (BrowserConsoleHandler) and then to the default StreamHandler.
In production, I want the errors and other critical messages to go to an e-mail then stored in the database or (if fails) to a log file (default StreamHandler)
I want to set up this in a global middle-ware that I have created, which looks like this now:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use Monolog\Logger;
use Monolog\Handler\BrowserConsoleHandler;
class GlobalConfig
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
// Get Monolog instance
$monolog = Log::getMonolog();
// In local environment the logs will be shown on browser
if (App::environment('local')) {
// Show logs in browser console
$monolog -> pushHandler(new BrowserConsoleHandler());
} else {
// Here we set up for production
}
Log::debug("Browser handler working");
return $next($request);
}
}
This doesn't work (the message is stored in the log file only, not shown on console). What I can't figure out is how to let the Log facade know about this new handler, because here, as is obvious, things are only changed within the function's scope. I know I can do it in bootstrap/app.php but isn't it to early to get the environment? Also, if I need to save logs to the database, it must already be connected, I guess
You can try to do next in bootstrap\app.php
if(env('APP_ENV') == 'local') {
$app->configureMonologUsing(function($monolog) use ($app) {
$monolog->pushHandler(
$handler = new \Monolog\Handler\RotatingFileHandler(
$app->storagePath().'/logs/laravel.log',
$app->make('config')->get('app.log_max_files', 30),
\Monolog\Logger::DEBUG
)
);
$handler->setFormatter(new \Monolog\Formatter\LineFormatter(null, null, true, true));
$monolog->pushHandler(new \Monolog\Handler\NativeMailerHandler(
'to#mail.com',
'Log::error!',
'from#mail.com'
));
});
}
Idea here is to fully control your logging on live/local. Bad news - configureMonologUsing replace all default laravel loggers, so you need to configure all your logs manually here.
Middleware isn't really the place for this, unless you wanted to actually make a particular log entry at some point in the request cycle.
Your call to pushHandler belongs in bootstrap/app.php (in Laravel 5.2), according to the docs.
I would think it would be possible to extract this logic out into a provider in case it becomes complicated and you need to move it out of bootstrap/app.php
solution here
BrowserConsoleHandler sends the script snipped after finishing the php
script by register_shutdown_function(). At this time, Laravel already
sent the full response to the browser. So the script snipped from
BrowseConsoleHandler gets generated but never sent to the browser.

Zend setting the authorization header for unit testing using PHPUnit

Recently I tried to test my REST API's using PHPUnit.
I am facing problem to send http authorization header for my test case.
Every time I do that I get an 403 response instead of 200
Here is my code :
<?php
use Zend\Test\PHPUnit\Controller\AbstractHttpControllerTestCase;
use Zend\Http\Request;
use Zend\Http\Headers;
use Zend\Http\Response;
class TrialTest extends AbstractHttpControllerTestCase
{
protected $traceError = true;
public function setUp()
{
$this->setApplicationConfig(
include 'config/application.config.php'
);
parent::setUp();
}
public function testAction()
{
$this->request = new Request();
$this->getRequest()->setMethod('GET');
//$headers = new \Zend\Http\Headers;
//$header = $headers->addHeader($headers->fromString('Authorization:Bearer test'));
$this->getRequest()->sendHeaders('Authorization:Bearer test');
//var_dump($headers);
//$this->getRequest()->setHeaders($header);
$this->dispatch('/campaign');
$this->assertResponseStatusCode(200);
}
}
Kindly help !! where am I going wrong ?
Try setting your headers like this:
$headers = new \Zend\Http\Headers;
$headers->addHeaderLine('Authorization', 'Bearer test');
$this->request->setHeaders($headers);
And you have to make sure that test a valid OAuth token otherwise it will never work. I am not so sure if a 4 character token will ever validate correctly...
UPDATE
I think there is a general problem with your test design. You only set the request object in the controller instance, but the service taking care of authentication has no access to this request object and thus it will not authorize the request correctly.
If you write a controller test in which you test the route '/campaign' you should only test the controller functionality and set mocks for all dependencies. I think the main problem starts in your setUp method. To test this controller you should not load your whole application.config.php. You should set an MvcEvent instance and attach all you need to this event (the correct Router instance, etc) and then dispatch the controller.
Check a proper example of such a ZF2 controller test here.
Testing your OAuth module should happen in an independent test.

In Laravel 5, why is Request::root() different when called during phpunit test?

I defined a test which tests the creation of a user. The controller is set to redirect back to the same page on error (using validation through a generated App\Http\Requests\Request). This works correctly when manually clicking in a browser, but fails during a test. Instead of being redirected to:
http://localhost/account/create
The test redirects to (missing a slash):
http://localhostaccount/create
Neither of these urls are what I have setup in the .htaccess or in the $url variable in config/app.php. Which is (On OSX Yosemite):
http://~username/laravel_projects/projectname/public
I finally pinpointed the issue to have something to do with how the result of Request::root() is generated. Making a call to this outside of a test results in the expected value defined in .htaccess and $url. Inside the test it results in:
http://localhost
What configuration needs to change in order to get this function to return the correct value in both contexts?
I should also mention I made the painful upgrade from Laravel 4 to the current version 5.0.27.
****** UPDATE *******
I was able to figure out an acceptable solution/workaround to this issue!
In Laravel 5, FormRequests were introduced to help move validation logic out of controllers. Once a request is mapped to the controller, if a FormRequest (or just Request) is specified, this is executed before hitting the controller action.
This FormRequest by default handles the response if the validation fails. It attempts to construct a redirect based on the route you posted the form data to. In my case, possibly related to an error of mine updating from Laravel 4 to 5, this default redirect was being constructed incorrectly. The Laravel System code for handling the response looks like this:
/**
* Get the proper failed validation response for the request.
*
* #param array $errors
* #return \Symfony\Component\HttpFoundation\Response
*/
public function response(array $errors)
{
if ($this->ajax() || $this->wantsJson())
{
return new JsonResponse($errors, 422);
}
return $this->redirector->to($this->getRedirectUrl())
->withInput($this->except($this->dontFlash))
->withErrors($errors, $this->errorBag);
}
Notice how the returned redirect is NOT the same as calling Redirect::route('some_route'). You can override this response function by including use Response in your Request class.
After using Redirect::route() to create the redirect, the logic in my tests passed with the expected results. Here is my Request code that worked:
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use App\Http\Requests\Request;
use Response;
class AccountRequest extends FormRequest {
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'email' => 'required|max:50|email|unique:users',
'password' => 'required|min:6',
'password_confirmation' => 'required|same:password'
];
}
public function response(array $errors){
return \Redirect::route('account_create');
}
}
The important part is that I called Redirect::route instead of letting the default response code execute.
Override the response function in the FormRequest validation handler to force the redirect to be constructed with Redirect::route('named_route') instead of allowing the default redirect.
You need to change config/app.php file's url value. Default value is http://localhost
Doc from config/app.php
This URL is used by the console to properly generate URLs when using the Artisan command line tool. You should set this to the root of your application so that it is used when running Artisan tasks.
I know this isn't an exact answer to your question since it is not a configuration update that solves the problem. But I was struggling with a related problem and this seems to be the only post on the internet of someone dealing with something similar - I thought I'd put in my two cents for anyone that wants a different fix.
Please note that I'm using Laravel 4.2 at the moment, so this might have changed in Laravel 5 (although I doubt it).
You can specify the HTTP_HOST header when you're testing a controller using the function:
$response = $this->call($method, $uri, $parameters, $files, $server, $content);
To specify the header just provided the $server variable as an array like so:
array('HTTP_HOST' => 'testing.mydomain.com');
When I did the above, the value produced for my Request::root() was http://testing.mydomain.com.
Again, I know this isn't a configuration update to solve you're issue, but hopefully this can help someone struggling with a semi-related issue.
If you tried changine config/app.php and it did not help.
it is better to use $_ENV - global variable in phpunit.
say, you want Request::root() to return 'my.site'
but you cannot touch phpunit.xml
you can simply set an env param like so
$_ENV['APP_URL'] = 'my.site';
and call $this->refreshApplication(); in your unittest.
viola, your request()->root() is giving you my.site now.

Hooking into the Error processing cycle

I'm building a monitoring solution for logging PHP errors, uncaught exceptions and anything else the user wants to log to a database table. Kind of a replacement for the Monitoring solution in the commercial Zend Server.
I've written a Monitor class which extends Zend_Log and can handle all the mentioned cases.
My aim is to reduce configuration to one place, which would be the Bootstrap. At the moment I'm initializing the monitor like this:
protected function _initMonitor()
{
$config = Zend_Registry::get('config');
$monitorDb = Zend_Db::factory($config->resources->db->adapter, $config->resources->db->params);
$monitor = new Survey_Monitor(new Zend_Log_Writer_Db($monitorDb, 'logEntries'), $config->projectName);
$monitor->registerErrorHandler()->logExceptions();
}
The registerErrorHandler() method enables php error logging to the DB, the logExceptions() method is an extension and just sets a protected flag.
In the ErrorController errorAction I add the following lines:
//use the monitor to log exceptions, if enabled
$monitor = Zend_Registry::get('monitor');
if (TRUE == $monitor->loggingExceptions)
{
$monitor->log($errors->exception);
}
I would like to avoid adding code to the ErrorController though, I'd rather register a plugin dynamically. That would make integration into existing projects easier for the user.
Question: Can I register a controller plugin that uses the postDispatch hook and achieve the same effect? I don't understand what events trigger the errorAction, if there are multiple events at multiple stages of the circuit, would I need to use several hooks?
Register your plugin with stack index 101. Check for exceptions in response object on routeShutdown and postDispatch.
$response = $this->getResponse();
if ($response->isException()) {
$exceptions = $response->getException();
}
to check if exception was thrown inside error handler loop you must place dispatch() in a try-catch block
The accepted answer by Xerkus got me on the right track. I would like to add some more information about my solution, though.
I wrote a Controller Plugin which looks like that:
class Survey_Controller_Plugin_MonitorExceptions extends Zend_Controller_Plugin_Abstract
{
public function postDispatch(Zend_Controller_Request_Abstract $request)
{
$response = $this->getResponse();
$monitor = Zend_Registry::get('monitor');
if ($response->isException())
{
$monitor->log($response);
}
}
}
Note that you get an Array of Zend_Exception instances if you use $response->getException(). After I had understood that, I simply added a foreach loop to my logger method that writes each Exception to log separately.
Now almost everything works as expected. At the moment I still get two identical exceptions logged, which is not what I would expect. I'll have to look into that via another question on SO.

Categories