I have a Laravel app that sends its exceptions to an Errbit server.
Here is the code for the exception handler:
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Session\TokenMismatchException;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Illuminate\Foundation\Validation\ValidationException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Airbrake\Notifier;
class Handler extends ExceptionHandler
{
protected $dontReport = [
AuthorizationException::class,
HttpException::class,
ModelNotFoundException::class,
ValidationException::class,
TokenMismatchException::class,
];
public function report(Exception $e)
{
if ($this->shouldReport($e)) {
$options = [
'environment' => app()->environment(),
'host' => config('airbrake.server'),
'projectId' => config('airbrake.api_key'),
'projectKey' => config('airbrake.api_key'),
'rootDirectory' => base_path(),
'secure' => TRUE,
'url' => request()->fullUrl(),
];
$notifier = new Notifier($options);
$notifier->notify($e);
}
return parent::report($e);
}
public function render($request, Exception $e)
{
// Replace `ModelNotFound` with 404.
if ($e instanceof ModelNotFoundException) {
$message = 'Page not found';
if (config('app.debug')) {
$message = $e->getMessage();
}
$e = new NotFoundHttpException($message, $e);
}
$response = parent::render($request, $e);
return $response;
}
}
While this works really well in general, I want to avoid logging errors that happen when I run artisan commands. The whole point of logging errors is to be notified of a problem, but if I'm sitting at the console, I already know about the problem.
I thought of looking in the stack trace to see if artisan is present there, but I see two problems with that:
It doesn't sound like a very efficient thing to be doing.
Queue listeners are also run through artisan, and I do need to get the exceptions from those, because they are not actually being run from the console.
How can I skip exception reporting for all exceptions run from the console, but keep it on for all the others?
The Illuminate\Foundation\Console\Kernel class that actually runs artisan commands has a function reportException which calls your ExceptionHandler's report method.
I added an override of that method to my Kernel class that checks if STDIN is an interactive terminal and disables the error reporting:
protected function reportException(Exception $e)
{
// Disable exception reporting if run from the console.
if (function_exists('posix_isatty') && #posix_isatty(STDIN)) {
echo "Not sending exception report";
return;
}
parent::reportException($e);
}
Related
On laravel 9 site in my custom class method raise custom error with error message
<?php
class CustomClass
{
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Exceptions\HttpResponseException;
...
public function method(): array
{
if(false) {
throw new HttpResponseException(response()->json([
'message' => $validated['message'],
], 422));
}
but I failed to catch this error in control where methods raised :
{
try {
$imageUploadSuccess = $customClass->method(
...
); // This method can raise different types of exception
}
catch (ModelNotFoundException $e) { // exception with invalid id is catched - THIS PART WORKS OK
return response()->json([
'message' => 'Item not found',
], 404);
}
catch (HttpResponseException $e) { // I catch HttpResponseException with is generated in method
// None of these 2 methods returns custom error message in method :
\Log::info($e->getMessage()); // empty value - I NEED TO GET THIS MESSAGE TEXT
\Log::info($e->getCode()); // 0 value
\Log::info($e->getTraceAsString());
return response()->json([
'message' => $e->getMessage(),
], 422);
}
return response()->json(['success' => true], 400);
Have I to use other class, not HttpResponseException ? I which way ?
Thanks in advance!
You’re not throwing your custom exception so it’s never raised so it won’t be catchable.
Replace where you throw the HttpResponseException with your Exception.
<?php
class CustomClass
{
public function method()
{
$isValid = false;
if(!$isValid) {
throw new SomeCustomException(response()->json([
'message' => $validated['message'],
], 422));
}
}
}
You would then use do something like:
$customClass = new CustomClass();
$customClass->method();
Note how I have defined a variable $isValid and how I check it for a true or false value in the if statement. This is because if checks for truthy values and false will never be `true so your custom exception would never be thrown.
if (false) {
// code in here will never execute
}
There are numerous places in Laravel Cashier where exceptions are trapped within the Cashier code, but not really handled in any meaningful way - not even logged. This results in bizarre behaviour if there is something wrong with the Stripe request (e.g. invalid price_id specified), where a null value is returned with no indication about why.
Anyone know why they have silently 'trapped' these errors making it impossible for the calling code to report?
e.g. Laravel\Cashier\Concerns\ManageInvoices::upcomingInvoice():
public function upcomingInvoice(array $options = [])
{
if (! $this->hasStripeId()) {
return;
}
$parameters = array_merge([
'automatic_tax' => $this->automaticTaxPayload(),
'customer' => $this->stripe_id,
], $options);
try {
$stripeInvoice = $this->stripe()->invoices->upcoming($parameters);
return new Invoice($this, $stripeInvoice, $parameters);
} catch (StripeInvalidRequestException $exception) {
//
}
}
I have a bit of a dilemma as I need to come up with a good logger that logs what is happening in the app at the same time if there is a Log::error called, it should also notify Devs and Sys admin via slack. It is currently working, but it adds an overhead to the request-response time.
Below is my setting:
//config/logging.php
'default' => env('LOG_CHANNEL', 'stack'),
//truncated
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['daily', 'slack'],
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => 'debug',
'days' => 0,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'App',
'emoji' => ':boom:',
'level' => 'error',
]
]
//truncated
//UserController
public function show(User $user)
{
//just a sample code, the important part is where the Log facade is called
try {
//business logic
} catch (Exception $e) {
Log::error(get_class(), [
'user_id' => $user->id,
'message' => $e->getMessage()
]);
}
return view('user.show', compact($user));
}
It is already working, but for sure we can still improve this to reduce the overhead somehow even though the added time for code above is negligible, but the real code is more complex and has quite a lot of iteration
How can I alter modify the behavior of the 'slack' logger to push it into a queue when it is triggered? I prefer to code it once and forget it rather than remembering that I have to push it to an on-demand logger such as
Log::chanel(['daily', 'slack'])->...
OR
//this is good for more on particular event notification but not not error notification which can happen anywhere
Notification::route('slack', env('LOG_SLACK_WEBHOOK_URL'))->notify(new AlertDevInSlackNotification)`
Note:
I tried adding some code into bootstrap/app.php but it is not working
//bootstrap/app.php
$app->configureMonologUsing(function($monolog) use ($app) {
//some code here. does not work, just getting page not working
});
It is like when I call this log level and this channel, I want it to be queued
You can do like this.
1.Create Job ex: name as LogSlackQueue.php
public class LogSlackQueue implements ShouldQueue {
...
...
public function handle() {
Log::channel(['daily', 'slack'])->info($your_input);
}
}
2.Then use as
LogSlackQueue::dispatch($your_input)
If you dont want to do like above, you need to figure it out to make custom provider
Thanks to #ZeroOne for giving out idea on how to solve it. I wanted it automatic and any existing code having Log::error() will automatically prompt the devs.
Below is my solution.
//CustomSlackServiceProvider.php
try {
//listen to all events
Event::listen('*', function($event, $details) {
//check if the event message logged event which is triggered when we call Log::<level>
if($event == "Illuminate\Log\Events\MessageLogged") {
//$details contain all the information we need and it comes in array of object
foreach($details as $detail) {
//check if the log level is from error to emergency
if(in_array($detail->level, ['emergency', 'critical', 'alert', 'error'])) {
//trigger the notification
Notification::route('slack', env('LOG_SLACK_WEBHOOK_URL'))->notify(new AlertDevInSlackNotification($detail->message, $detail->level, $detail->context));
}
}
}
});
} catch (Exception $e) {
}
//AlertDevInSlackNotification.php
class AlertDevInSlackNotification extends Notification implements ShouldQueue
{
use Queueable;
private $class;
private $level;
private $context;
public function __construct($class, $level, $context)
{
$this->class = $class;
$this->level = strtoupper($level);
$this->context = $context;
//prevent congestion in primary queue - make sure this queue exists
$this->queue = 'alert';
}
public function via($notifiable)
{
return ['slack'];
}
public function toSlack($notifiable)
{
return (new SlackMessage)
->content($this->level.': '.$this->class)
->attachment(function($attachment) {
$attachment->fields($this->context);
});
}
UPDATE:
The code above will work when you trigger Log::error().
But to listen to an event that is being thrown by an error such as syntax error which will cause "Serialization of 'Closure' is not allowed". You can do this instead to improve coverage:
public function boot()
{
try {
//listen to all events
Event::listen('*', function($event, $details) {
//check if the event message logged event which is triggered when we call Log::<level>
if($event == "Illuminate\Log\Events\MessageLogged") {
// dump($event);
//$details contain all the information we need and it comes in array of object
foreach($details as $detail) {
$this->path = '';
$this->level = '';
$this->context = [];
$this->message = '';
//check if the log level is from error to emergency
if(in_array($detail->level, ['emergency', 'critical', 'alert', 'error'])) {
//#todo - exclude: Error while reading line from the server. [tcp://cache:6379] = restart
//check if the context has exception and is an instance of exception
//This is to prevent: "Serialization of 'Closure' is not allowed" which prevents jobs from being pushed to the queue
if(isset($detail->context['exception'])) {
if($detail->context['exception'] instanceof Exception) {
$this->level = $detail->level;
//to keep consistency on all the log message, putting the filename as the header
$this->message = $detail->context['exception']->getFile();
$this->context['user'] = auth()->check() ? auth()->user()->id.' - '. auth()->user()->first_name.' '.auth()->user()->last_name : null;
$this->context['message'] = $detail->context['exception']->getMessage();
$this->context['line'] = $detail->context['exception']->getLine();
$this->context['path'] = request()->path();
$this->runNotification();
continue;
}
}
$this->level = $detail->level;
$this->context = $detail->context;
$this->message = $detail->message;
$this->runNotification();
continue;
}
}
}
});
} catch (Exception $e) {
}
}
public function runNotification()
{
Notification::route('slack', env('LOG_SLACK_WEBHOOK_URL'))->notify(new AlertDevInSlackNotification($this->message, $this->level, $this->context));
}
I try this code to set custom validation messages, but with no effect -
class TestController extends Controller
{
public function submit(Request $request)
{
$this->validate($request,
[
'items' => 'required'
],
[
'items.required' => 'test test'
]
);
}
}
But on error I got this response -
{
"error": "The given data failed to pass validation."
}
What wrong with this code?
UPD:
Earlier I edit App\Exceptions\Handler to put errors(in API response) in specific format -
{
"error": "123"
}
This code is reason that validation errors not shown -
public function render($request, Exception $e)
{
return response([
'error' => $e->getMessage()
], 500);
}
I update Handler::render method regarding to this purpose
public function render($request, Exception $e) {
$response = parent::render($request, $e);
if (isset($response->exception) and !empty($response->exception)) {
return response(['error' => $response->exception->getMessage()], 500);
} else {
return parent::render($request, $e);
}
}
But I think I need to improve this code.
It seems you mixed validation rules and messages.
The validate method takes 3 parameter: request, rules, messages.
Please try this:
public function submit(Request $request)
{
$rules = [
'items' => 'required',
'otheritems' => 'required',
];
$messages = [
'items.required' => 'Error: Please enter something.',
'otheritems.required' => 'Otheritems are also required',
];
$this->validate($request, $rules, $messages);
}
The latest Lumen version always gives back JSON, see documentation:
The $this->validate helper will always return JSON responses with the relevant error messages when validation fails. If you are not building a stateless API that solely sends JSON responses, you should use the full Laravel framework.
Update regarding error:
The given result by Lumen looks like that.
{"items":["Items are required"],"otheritems":["Otheritems are also required"]}
Each item that failed the validation gets an entry in your response. So your error bag, need to be a JSON array.
Custom exception render method:
public function render($request, Exception $e)
{
$response = parent::render($request, $e);
if ($response->getStatusCode() == 422) {
$renderResult = parent::render($request, $e);
$returnResult['error'] = json_decode($renderResult->content(), true);
$returnResult = json_encode($returnResult);
return new Response($returnResult, $response->getStatusCode());
} else {
return parent::render($request, $e);
}
}
im trying to disable layout while showing error/exception page in my zf2 module
but nothing works
please help
Final Solution in my Module.php
$eventManager = $e->getApplication()->getEventManager();
$eventManager->attach(MvcEvent::EVENT_DISPATCH_ERROR, function($e) {
$result = $e->getResult();
$result->setTerminal(TRUE);
});
it works and only loads error/404 view file not the layout file
thanks andrew
If you look at the Zend Framework 2 MVC module you will see possibilities for this..
DispatchListener.php
try {
$return = $controller->dispatch($request, $response);
} catch (\Exception $ex) {
$e->setError($application::ERROR_EXCEPTION)
->setController($controllerName)
->setControllerClass(get_class($controller))
->setParam('exception', $ex);
// look here...
$results = $events->trigger(MvcEvent::EVENT_DISPATCH_ERROR, $e);
$return = $results->last();
if (! $return) {
$return = $e->getResult();
}
}
You can see they MvcEvent which is triggered when you have an exception thrown inside the controller, there's a few other processes attaching to this event.
You could attach a method to this event and do what ever you want.
As an example look at ExceptionStrategy.php
public function prepareExceptionViewModel(MvcEvent $e)
{
....
}
this is not your ans but it will help to other
in zf2
public function indexAction()
{
echo "json"
return $this->getResponse();
}
The easiest way is to use config config/autoload/local.php
return array(
'view_manager' => array(
'display_exceptions' => false
)
);
Adding this lines disables exceptions. In addition you can use your own local.php on a dev server.