Monolog contains elastic search handler and formatter, but it's implementation to laravel as a custom channel is not as straightforward as described on Laravel documentation web site.
Here's a brief step by step instruction how to do that.
Create a config file for your elastic search logging.
config/elastic_log.php
with the next content:
<?php
return [
'host' => env('ELASTIC_HOST'),
'index' => 'index_name',
'prefix' => 'index_prefix',
'type' => '_doc',
];
You can change your index name and prefix to any string values.
In your .env file put your elastic host address:
ELASTIC_HOST=your_elastic_host:port
Install elasticsearch/elasticsearch official package
composer require elasticsearch/elasticsearch
Create ElasticLogging service provider
php artisan make:provider ElasticLogProvider
With the following content:
<?php
namespace App\Providers;
use Elasticsearch\Client;
use Elasticsearch\ClientBuilder;
use Illuminate\Support\ServiceProvider;
use Monolog\Formatter\ElasticsearchFormatter;
use Monolog\Handler\ElasticsearchHandler;
class ElasticLogProvider extends ServiceProvider
{
/**
* Register services.
*
* #return void
*/
public function register()
{
//
}
/**
* Bootstrap services.
*
* #return void
*/
public function boot()
{
$index = rtrim(config('elastic_log.prefix'), '_') . '_' . config('elastic_log.index');
$type = config('elastic_log.type');
$this->app->bind(Client::class, function ($app) {
return ClientBuilder::create()->setHosts([config('elastic_log.host')])->build();
});
$this->app->bind(ElasticsearchFormatter::class, function ($app) use ($index, $type) {
return new ElasticsearchFormatter($index, $type);
});
$this->app->bind(ElasticsearchHandler::class, function ($app) use ($index, $type) {
return new ElasticsearchHandler($app->make(Client::class), [
'index' => $index,
'type' => $type,
'ignore_error' => false,
]);
});
}
}
Add this provider to your app.php config file to the providers array:
App\Providers\ElasticLogProvider::class,
Create a command for elastic logging settings on the server. This command creates an index on the server if it doesn't exist yet.
Now for the preparation of your server, just run elastic:log_setup;
php artisan make:command ElasticLogSetup
With the following content:
<?php
namespace App\Console\Commands;
use Elasticsearch\Client;
use Illuminate\Console\Command;
class ElasticLogSetup extends Command
{
/**
* #var Client
*/
protected $client;
/**
* The name and signature of the console command.
*
* #var string
*/
protected $signature = 'elastic:log_setup';
/**
* The console command description.
*
* #var string
*/
protected $description = 'Setup elastic log index';
/**
* Create a new command instance.
*
* #return void
*/
public function __construct(Client $client)
{
$this->client = $client;
parent::__construct();
}
/**
* Execute the console command.
*
* #return int
*/
public function handle()
{
$index = rtrim(config('elastic_log.prefix'), '_') . '_' . config('elastic_log.index');
if (!$this->client->indices()->exists(['index' => $index])) {
$this->client->indices()->create([
'index' => $index,
]);
}
}
}
In the file config/logging.php add this element to 'channels' array and import the related classes:
use Monolog\Formatter\ElasticsearchFormatter;
use Monolog\Handler\ElasticsearchHandler;
'elastic' => [
'driver' => 'monolog',
'handler' => ElasticsearchHandler::class,
'level' => 'debug',
'formatter' => ElasticsearchFormatter::class,
];
Now you can use the channel 'elastic' or change it in your .env settings as a default channel:
LOG_CHANNEL=elastic
From now on, you can use standard laravel Log facade to send the information to your ElasticSearch server
Related
Hello I am fairly new to Laravel and have ran into an issue with logging complete artisan commands. Here is the listener I registered for logging my commands:
protected $listen = [
CommandStarting::class => [CommandLogging::class],
];
Here is the code for the listener:
<?php
namespace App\Listeners;
use Illuminate\Console\Events\CommandStarting;
class CommandLogging
{
/**
* Create the event listener.
*
* #return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* #param CommandStarting $event
* #return void
*/
public function handle(CommandStarting $event): void
{
$log->info([
'commandName' => $event->command,
'input' => $event->input->getArguments(),
'output' => $event->output,
]);
}
}
My issue is when I run a command such as php artisan make:event FakeEventTest the only thing I get is the command name. Is there anyway to get the arguments as well such as FakeEventTest in the example command.
I'm trying to send an email using a command in Laravel. I would like to send a file from a specific folder. Previously I did it using a view with a form, but now I want to send the email using a command. The file will always be in the same folder.
This is the command code:
<?php
namespace efsystem\Console\Commands;
use Illuminate\Console\Command;
use Storage;
use Mail;
use Config;
class SendEmailEfsystem extends Command
{
/**
* The name and signature of the console command.
*
* #var string
*/
protected $signature = 'emails:send';
/**
* The console command description.
*
* #var string
*/
protected $description = 'Sending emails to the users';
/**
* Create a new command instance.
*
* #return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* #return mixed
*/
public function handle()
{
$data = array(
'destino' => 'example#gmail.com',
'asunto' => 'example',
);
Mail::send('administracion.email.email_body', $data, function ($message) use ($data) {
$message->to($data['destino']);
$message->subject($data['asunto']);
$message->from(Config::get('mail.username'));
});
$this->info('The emails are send successfully!');
}
}
Since in the "form" you used $request['a_file'] the variable was an instance of Symfony\Component\HttpFoundation\File\UploadedFile wich is an extention of Symfony\Component\HttpFoundation\File\File.
what you need to do is instantiate a File class with the path you have.
$data = array(
'destino' => 'example#gmail.com',
'asunto' => 'example',
'a_file' => new \Symfony\Component\HttpFoundation\File\File($pathToFile, true)
);
You can use N69S a_file answer
This is basic tips to help you run your command.
Your Kernel.php must be like this
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* #var array
*/
protected $commands = [
Commands\SendEmailEfsystem::class,
];
/**
* Define the application's command schedule.
*
* #param \Illuminate\Console\Scheduling\Schedule $schedule
* #return void
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('emails:send')->everyMonth();
}
/**
* Register the Closure based commands for the application.
*
* #return void
*/
protected function commands()
{
require base_path('routes/console.php');
}
}
Then if you want to run it. Simply run php artisan emails:send
or You want to run it using code you can use Artisan::call('emails:send);
Is there a way to get a response from Mandrill using the mail function (or any other function) in Laravel 4?
Using the code below the message sends fine but just returns null:
$response = Mail::send('emails.test', [], function($message)
{
$message->to('test#email.com')->subject('test email');
});
dd($response);
I tried using both the smtp driver and Mandrill driver but it didn't make any difference
Also had some issues with logging the fetching _id from a Mandrill sent event.
Created a workaround for Laravel 5.7;
Create a CustomMailServiceProvider
<?php
// app/Providers/CustomMailServiceProvider.php
namespace App\Providers;
use App\Misc\Transport\CustomMandrillTransport;
use Swift_Mailer;
use Illuminate\Support\Arr;
use GuzzleHttp\Client as HttpClient;
class CustomMailServiceProvider extends \Illuminate\Mail\MailServiceProvider {
public function register(){
parent::register();
$this->registerMandrillMailer();
}
/**
* Register the CustomMandrill Swift Transport instance.
*
* #param array $config
* #return void
*/
protected function registerMandrillMailer()
{
if ($this->app['config']['mail.driver'] == 'mandrill') {
$this->app->singleton('swift.mailer', function ($app) {
$mandrillConfig = $app['config']->get('services.mandrill', []);
return new Swift_Mailer(new CustomMandrillTransport( $this->guzzle($mandrillConfig), $mandrillConfig['secret']));
});
}
}
/**
* Get a fresh Guzzle HTTP client instance.
*
* #param array $config
* #return \GuzzleHttp\Client
*/
protected function guzzle($config)
{
return new HttpClient(Arr::add(
$config['guzzle'] ?? [], 'connect_timeout', 60
));
}
}
Create a CustomMandrillTransport
<?php
// app/Misc/Transport/CustomMandrillTransport.php
namespace App\Misc\Transport;
use Swift_Mime_SimpleMessage;
class CustomMandrillTransport extends \Illuminate\Mail\Transport\MandrillTransport {
public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null)
{
$this->beforeSendPerformed($message);
$response = $this->client->request('POST', 'https://mandrillapp.com/api/1.0/messages/send-raw.json', [
'form_params' => [
'key' => $this->key,
'to' => $this->getTo($message),
'raw_message' => $message->toString(),
'async' => true,
],
]);
// Lets replace body by actually something useful -_-
$message->setBody((string)$response->getBody());
$this->sendPerformed($message);
return $this->numberOfRecipients($message);
}
}
Create an email Listener
<?php
// app/Listeners/EmailSentListener.php
namespace App\Listeners;
use Illuminate\Mail\Events\MessageSent;
use Mail;
class EmailSentListener
{
/**
* Create the event listener.
*
* #return void
*/
public function __construct()
{
}
/**
* Handle the event.
*
* #param \App\Events\OrderShipped $event
* #return void
*/
public function handle(MessageSent $event)
{
$mandrillBody = $event->message->getBody(); // [{"email":"xxxxx#gmail.com","status":"queued","_id":"19219cfd3e0e4133aed48214ebb4ed71"}]
}
}
In config/app.php comment original
// Illuminate\Mail\MailServiceProvider::class,
And Add your own:
App\Providers\CustomMailServiceProvider::class,
And make sure to listen to sent Event
<?php
namespace App\Providers;
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* #var array
*/
protected $listen = [
'App\Events\SomeEvent' => [
'App\Listeners\EventListener',
],
'Illuminate\Mail\Events\MessageSent' => [
'App\Listeners\EmailSentListener',
]
];
}
Hope this helps more people
It does not appear that you can. The MandrillTransport::send does not return anything and it does not expose the HttpClient.
I am trying to write a laravel command but I can't have the options to be mandatory.
I do understand that the concept of option is being "optional" but I'd like to have a command in which it is clear which input you are inserting and in no particular order.
i.e.
I would like to achieve this, with par2 and par 2 mandatory
$command-abc --par1=value1 --par2=value2
Instead of:
$command-abc value1 value2
So far this is the signature I used:
protected $signature = 'dst-comparison:analyse
{--client : Client Name}
{--clientId : Client ID}
{--recordingId : Client Recording ID}
{--CSVFile : Path to the downloaded CSV file from Spotify analytics}
{--dataUpdateTo=null : CSV Data will be truncated from this date onwards}';
Following the Laravel documentation (https://laravel.com/docs/5.1/artisan) and this guide: http://code.tutsplus.com/tutorials/your-one-stop-guide-to-laravel-commands--net-30349 it seemed that overwriting the getOptions method was doing the trick, but it is not working for me.
/**
* Get the console command options.
*
* #return array
*/
protected function getOptions()
{
return array(
array('client', null, InputOption::VALUE_REQUIRED, 'Client Name'),
array('clientId', null, InputOption::VALUE_REQUIRED, 'Client ID'),
array('recordingId', null, InputOption::VALUE_REQUIRED, 'Client Recording ID'),
array('CSVFile', null, InputOption::VALUE_REQUIRED, 'Path to the downloaded CSV file from Spotify analytics'),
array('dataUpdateTo', null, InputOption::VALUE_OPTIONAL, 'CSV Data will be truncated from this date onwards')
);
}
Any ideas?
I think you will have to take care of mandatory input yourself. Have a look at the various output functions $this->error(...) and check if all necessary input was given $this->option('client'); (<- returns null if no input was defined)
https://laravel.com/docs/master/artisan
If you'd like to make something more generic and take advantage of Laravel Validation https://laravel.com/docs/8.x/validation, follow this approach:
create an abstract BaseCommand.php, and add Validator there
extend that BaseCommand in your custom command myCustomCommand.php
#BaseCommand.php
<?php
namespace Your\NameSpace;
use Illuminate\Console\Command;
use Validator;
abstract class BaseCommand extends Command {
public $rules;
/**
* Create a new command instance.
*
* #return void
*/
public function __construct($rules) {
parent::__construct();
$this->rules = $rules;
}
/**
* Execute the console command.
*
* #return mixed
*/
public function handle() {
if ($this->validate()) {
$this->handleAfterValidation();
}
}
public function validate() {
$validator = Validator::make($this->option(), $this->rules);
if (!$validator->passes()) {
$this->error($validator->messages());
return false;
}
return true;
}
abstract public function handleAfterValidation();
}
#myCustomCommand.php
<?php
namespace Your\NameSpace;
use Validator;
use Your\NameSpace\BaseCommand;
class SendEmail extends BaseCommand {
public $rules = [
'email' => 'required|email',
'age' => 'required|numeric',
'name' => 'required|string'
];
/**
* The name and signature of the console command.
*
* #var string
*/
protected $signature = 'mycmd:Whatever {--email=} {--age=} {--name=}';
/**
* The console command description.
*
* #var string
*/
protected $description = 'Add description';
/**
* Create a new command instance.
*
* #return void
*/
public function __construct() {
parent::__construct($this->rules);
}
public function handleAfterValidation() {
// DO something here with $this->option('email'), $this->option('age'), $this->option('name')
}
}
Consider the following task:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Foundation\Inspiring;
use App\Etis\Domain\Services\TwitterService;
use \Twitter;
use Log;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
class FetchTweets extends Command
{
/**
* The name and signature of the console command.
*
* #var string
*/
protected $signature = 'fetch_tweets';
/**
* The console command description.
*
* #var string
*/
protected $description = 'fetches the latest ten tweets';
/**
* Execute the console command.
*
* #return mixed
*/
public function handle()
{
$logInstance = Log::getMonolog();
$logInstance->pushHandler(new StreamHandler(storage_path('logs/tweets.log'), Logger::INFO));
$tweets = Twitter::getUserTimeline([
'screen_name' => env('TWITTER_USER_NAME'),
'count' => env('TWITTER_TWEET_AMOUNT'),
'format' => 'json'
]);
$logInstance->addInfo('Tweets', [$tweets]);
$twitterService = new TwitterService();
$twitterService->processTweets(json_decode($tweets, true));
}
}
Which is then set up as such:
$schedule->command('fetch_tweets')
->everyMinute()
->withoutOverlapping()
->appendOutputTo('storage/logs/tweets.log');
When I look, on production and even in local, I see that both the laravel.log and the tweets.log file have the contents that I printing out to tweets.log.
Why is this? How do I make it ONLY print out to tweets.log?
pushHandler() does not replace existing log handler. Instead, it adds a new log handler to the existing, predefined list of handlers. That's the reason why you're now getting your message logged in 2 log files now.
You need to call setHandlers() to overwrite the list of handlers:
$handler = new StreamHandler(storage_path('logs/tweets.log'), Logger::INFO);
$logInstance = Log::getMonolog();
$logInstance->setHandlers(array($handler));