Force slack logging into queue - laravel

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));
}

Related

Catching org_internal 403 error via Google's OAUTH?

I have google OATH setup via socialite (only for within our organisation) and everything is working fine.
One thing I'd like to try and do is catch this "error" and get redirected back to our login page with a custom message telling the user that they do not belong to our organisation.
In principle this works fine, they can just hit the back button... but for fluidity and design, I'd like to catch this and redirect back to our home page.
Is this even possible? If so, how would you recommend I go about it?
public function show()
{
return view('auth.login');
}
public function redirectToProvider($driver)
{
if( ! $this->isProviderAllowed($driver) ) {
return $this->sendFailedResponse("{$driver} is not currently supported");
}
try {
return Socialite::driver($driver)->redirect();
} catch (Exception $e) {
return $this->sendFailedResponse($e->getMessage());
}
}
public function handleProviderCallback( $driver )
{
try {
$user = Socialite::driver($driver)->user();
} catch (Exception $e) {
return $this->sendFailedResponse($e->getMessage());
}
// check for email in returned user
return empty( $user->email )
? redirect()->intended('/login?failed=1')
: $this->loginOrCreateAccount($user, $driver);
}
protected function sendSuccessResponse()
{
return redirect()->intended('/');
}
protected function sendFailedResponse($msg = null)
{
return redirect()->intended('/login?failedResponse='.$msg);
}
protected function loginOrCreateAccount($providerUser, $driver)
{
// check for already has account
$user = User::where('email', $providerUser->getEmail())->first();
// if user
if( $user ) {
// update the avatar and provider that might have changed
$user->update([
'avatar' => $providerUser->avatar,
'provider' => $driver,
'provider_id' => $providerUser->id,
'access_token' => $providerUser->token
]);
} else {
return redirect()->intended('/login?noUser=1');
}
// login the user
Auth::login($user, true);
return $this->sendSuccessResponse();
}
private function isProviderAllowed($driver)
{
return in_array($driver, $this->providers) && config()->has("services.{$driver}");
}

Laravel - How to update Input Array without deleting Sales Detail

In my Laravel-8 project, I have this controller for Input Field Array Update.
Controller:
public function update(UpdateSaleRequest $request, $id)
{
try {
$sale = Sale::find($id);
$data = $request->all();
$update['date'] = date('Y-m-d', strtotime($data['date']));
$update['company_id'] = $data['company_id'];
$update['name'] = $data['name'];
$update['remarks'] = $data['remarks'];
$sale->update($update);
SaleDetail::where('sale_id', $sale->id)->delete();
foreach ($data['invoiceItems'] as $item) {
$details = [
'sale_id' => $sale->id,
'item_id' => $item['item_id'],
'employee_id' => $item['employee_id'],
'quantity' => $item['qty'],
'price' => $item['cost'],
'total_price' => $item['cost'] * $item['qty'],
'sale_type_id'=>$item['sale_type_id']
];
$saleDetail = new SaleDetail($details );
$saleDetail->save();
}
} catch (JWTException $e) {
throw new HttpException(500);
}
return response()->json($sale);
}
In the form, the user can add more Sales Detail or remove.
Some of the SaleDetail fields are being used somewhere else.
Is there a way to update the input field array without deleting the SaleDetail as shown in what I did here:
SaleDetail::where('sale_id', $sale->id)->delete();
Thanks
I've tried to restructure your code so that's easier to edit. I've left some comments. I can really recommend refactoring.guru. There you will find many ways to improve your code so that it is more extensible, maintainable and testable. If you have any questions, please feel free to ask.
class Sale extends Model
{
// Use a relationship instead of building your own query
public function details() {
return $this->hasMany(SaleDetail::class);
}
}
class SaleDetail extends Model
{
// Use a computed property instead of manually calculating total price
// You can access it with $saleDetail->totalPrice
public function getTotalPriceAttribute() {
return $this->price * $this->quantity;
}
}
class UpdateSaleRequest extends Request
{
public function authorize() {
return true;
}
protected function prepareForValidation() {
$this->merge([
// Create a Carbon instance by string
'date' => Carbon::make($this->date)
]);
}
public function rules() {
// Your validation rules
// Please also validate your invoice items!
// See https://laravel.com/docs/8.x/validation#validating-arrays
}
}
// We let Laravel solve the sale by dependency injection
// You have to rename the variable name in ihr web.php
public function update(UpdateSaleRequest $request, Sale $sale)
{
// At this point, all inputs are validated!
// See https://laravel.com/docs/8.x/validation#creating-form-requests
$sale->update($request->validated());
// Please ensure, that all properties have the same name
// In your current implementation you have price = cost, be consistent!
foreach($request->input('invoiceItems') as $invoiceItem) {
// How we can consider that a detail is already created?
// I assume that each item_id will only occur once, otherwise you'll
// place the id of each detail in your update form (e.g. in a hidden input)
$candidate = $sale->details()
->where('item_id', $properties['item_id'])
->first();
if($candidate) {
$candidate->update($properties);
} else {
$sale->details()->create($properties);
}
}
// A JWT-Exception should not be necessary, since your authentication
// will be handled by a middleware.
return response()->json($sale);
}
I have not tested the code, few adjustments may be needed.
Laravel has a method called updateOrCreate as follow
/**
* Create or update a record matching the attributes, and fill it with values.
*
* #param array $attributes
* #param array $values
* #return \Illuminate\Database\Eloquent\Model|static
*/
public function updateOrCreate(array $attributes, array $values = [])
{
return tap($this->firstOrNew($attributes), function ($instance) use ($values) {
$instance->fill($values)->save();
});
}
That means you could do some thing like
public function update(UpdateSaleRequest $request, $id)
{
try {
$sale = Sale::find($id);
$data = $request->all();
$update['date'] = date('Y-m-d', strtotime($data['date']));
$update['company_id'] = $data['company_id'];
$update['name'] = $data['name'];
$update['remarks'] = $data['remarks'];
$sale->update($update);
foreach ($data['invoiceItems'] as $item) {
$details = [
'item_id' => $item['item_id'],
'employee_id' => $item['employee_id'],
'quantity' => $item['qty'],
'price' => $item['cost'],
'total_price' => $item['cost'] * $item['qty'],
'sale_type_id'=>$item['sale_type_id']
];
$sale->saleDetail()->updateOrCreate([
'sale_id' => $sale->id
], $details);
}
} catch (JWTException $e) {
throw new HttpException(500);
}
return response()->json($sale);
}
I would encourage you to refactor and clean up your code.You can also read more about it here https://laravel.com/docs/8.x/eloquent#upserts

How to flash validation errors to session in Laravel

The built in behavior for flashing back validation errors in Laravel does not seem to be working for my use case.
I have a (React) form that posts it's data via fetch API using this method, which reloads or redirects the page with (hopefully) any session data after the response is returned:
fetch(props.register_route, {
method: 'POST',
headers: {
'X-CSRF-Token': props.csrf,
},
body: data,
})
.then((result) => {
return result.json();
})
.then((result) => {
console.log(result);
window.location.href = result.url;
},
(error) => {
console.log(error);
});
In my controller, I validate this data but if I structure it as follows, the errors are not available as $errors in the resulting page
if ($validator->fails()) {
return redirect()->back()->withErrors($validator);
}
However if I manually flash the errors to the session and return a url instead of a redirect, suddenly the behavior works.
if ($validator->fails()) {
Session::flash('errors', $validator->errors());
return response->json([
'url' => route('register'),
], Response::HTTP_NOT_ACCEPTABLE);
}
I feel as if I must be doing something incorrectly here to have to use this workaround. I could also manually send the errors back in the response, which may be the right way to structure things in the long run.
when you are calling api from javascript or front end applications like Reactjs,Angular,android etc.. .So it expect return result should be in json format so it should be like
if ($validator->fails()) {
return response()->json( $validator->errors(),422);
}
if you not calling Method from direct laravel blade then pass response in JOSN Format.
like
https://laravel.com/docs/8.x/responses#json-responses
Or
make one ResponseManager File
<?PHP
namespace App\Libraries\utils;
class ResponseManager {
public static $response = array('flag' => true, 'data' => '', 'message' => '', 'code' => 01,);
public static function getError($data = '', $code = 10, $message = '', $flag = false) {
self::$response['flag'] = $flag;
self::$response['code'] = $code;
self::$response['data'] = $data;
self::$response['message'] = $message;
return self::$response;
}
public static function getResult($data = '', $code = 10, $message = '', $flag = true) {
self::$response['flag'] = $flag;
self::$response['code'] = $code;
self::$response['data'] = $data;
self::$response['message'] = $message;
return self::$response;
}}
Define in config/app.php
//custom class
'ResponseManager' => App\Libraries\utils\ResponseManager::class,
and then use in whole project
Error Message Like
if ($validation->fails()) {
$message = $validation->messages()->first();
return Response()->json(ResponseManager::getError('', 1, $message));
}
Success Message Like
return Response()->json(ResponseManager::getResult(null, 10, "Success"));

How to reinitialize model when client side validation fails in Yii 2?

I am working on Yii 2 form and I want to reinitialize model when client side validation fails. For example with certain rules like below:
public function rules()
{
return [
[['username'], 'required', 'message' => 'You must enter your username'],
['username','email'],
[['password'], 'required', 'message' => 'You must enter your password'],
];
}
When validation fails I want all fields to be empty (for example when user enters invalid email address). How can I do that?
I assume you use standard Yii 2 way of loading the model:
$model = new SomeModel();
if ($model->load(\Yii::$app->request->post()) && $model->save()) {
// ...
}
return $this->render('view', ['model' => $model]);
Set fields to null when validation fails. You don't want to create new instance (which would be easier) because you would lost all validation messages.
$model = new SomeModel();
if ($model->load(\Yii::$app->request->post())) {
if ($model->save()) {
// ....
} else {
$model->username = null;
$model->password = null;
}
}
return $this->render('view', ['model' => $model]);
UPDATE: for the client side validation add this JS code in view:
$("#form-ID").on("afterValidateAttribute", function (event, attribute, messages) {
if (event.result === false) {
attribute.value = "";
}
});
Replace #form-ID with proper form element JS identifier.

Lumen can't set custom validation rules

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);
}
}

Resources