Projeto inicial.

Foi desenvolvido inicialmente no gitlab
production
parent b324c88d0d
commit 5f06cb4c43

9
.gitignore vendored

@ -0,0 +1,9 @@
### Laravel template
/vendor
storage/*.key
.env
.idea
.phpunit.result.cache
.php-cs-fixer.cache
tools/php-cs-fixer/vendor
tools/php-cs-fixer/.php_cs.cache

@ -0,0 +1,38 @@
{
"name": "ae3/laravel-logs-layer",
"description": "Lib utilizada pelas aplicações laravel para padronização de logs.",
"type": "project",
"license": "MIT",
"require": {
"php": "^7.4",
"laravel/framework": "5.8.*",
"hashids/hashids": "^4.1",
"ext-json": "*",
"spatie/data-transfer-object": "v2.x-dev",
"pusher/pusher-php-server": "~4.0",
"guzzlehttp/guzzle": "^7.0"
},
"autoload": {
"psr-4": {
"Ae3\\LaravelLogsLayer\\": "src/",
"Ae3\\LaravelLogsLayer\\Tests\\": "tests/"
}
},
"authors": [
{
"name": "José Tobias de Freitas Neto"
}
],
"minimum-stability": "dev",
"extra": {
"laravel": {
"providers": [
"Ae3\\LaravelLogsLayer\\app\\Providers\\LogsLayerServiceProvider"
]
}
},
"require-dev": {
"phpunit/phpunit": "^7.5",
"orchestra/testbench": "3.8.x-dev"
}
}

6514
composer.lock generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
<php>
<server name="APP_ENV" value="testing"/>
</php>
</phpunit>

@ -0,0 +1,58 @@
<?php
namespace Ae3\LaravelLogsLayer\app\Containers;
class LogDataContainer
{
/**
* @var array
*/
private array $capturedQueries = [];
/**
* @var array
*/
private array $capturedHttpClientEvents = [];
/**
* @param array $queryInfo
* @return void
*/
public function addCapturedQuery(array $queryInfo)
{
$this->capturedQueries[] = $queryInfo;
}
/**
* @param array $httpClientEvent
* @return void
*/
public function addCapturedHttpClientEvent(array $httpClientEvent)
{
$this->capturedHttpClientEvents[] = $httpClientEvent;
}
/**
* @return array
*/
public function getCapturedHttpClientEvents(): array
{
return $this->capturedHttpClientEvents;
}
/**
* @return array
*/
public function getCapturedQueries(): array
{
return $this->capturedQueries;
}
/**
* @return void
*/
public function clearCapturedData()
{
$this->capturedQueries = [];
$this->capturedHttpClientEvents = [];
}
}

@ -0,0 +1,56 @@
<?php
namespace Ae3\LaravelLogsLayer\app\DataTransferObjects;
use Spatie\DataTransferObject\DataTransferObject;
class ExceptionContextDTO extends DataTransferObject
{
/**
* @var string|null
*/
public ?string $code;
/**
* Apenas o primeiro nome do namespace do seu projeto
* @var string|null
*/
public ?string $root_namespace;
/**
* @var array|null
*/
public ?array $tags;
/**
* @var object|null
*/
public ?object $current_user;
/**
* @var string|null
*/
public ?string $current_url;
/**
* @var array|null
*/
public ?array $custom_data;
/**
* @param array $data
* @return self
*/
public static function fromArray(array $data): self
{
return new self([
'code' => $data['code'] ?? null,
'root_namespace' => $data['root_namespace'] ?? null,
'tags' => $data['tags'] ?? null,
'current_user' => $data['current_user'] ?? null,
'current_url' => $data['current_url'] ?? null,
'custom_data' => $data['custom_data'] ?? null,
]);
}
}

@ -0,0 +1,34 @@
<?php
namespace Ae3\LaravelLogsLayer\app\DataTransferObjects;
use Spatie\DataTransferObject\DataTransferObject;
class LoggedExceptionDTO extends DataTransferObject
{
/**
* @var string
*/
public string $code;
/**
* @var string
*/
public string $message;
/**
* @var string
*/
public string $level;
/**
* @param array $data
* @return self
*/
public static function fromArray(array $data): self
{
return new self([
'code' => $data['code'],
'message' => $data['message'],
'level' => $data['level'],
]);
}
}

@ -0,0 +1,30 @@
<?php
namespace Ae3\LaravelLogsLayer\app\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Psr\Http\Message\RequestInterface;
class GuzzleEventCaptured
{
use Dispatchable;
/**
* @var RequestInterface
*/
public RequestInterface $request;
/**
* @var array
*/
public array $options;
/**
* @param RequestInterface $request
* @param array $options
*/
public function __construct(RequestInterface $request, array $options)
{
$this->request = $request;
$this->options = $options;
}
}

@ -0,0 +1,9 @@
<?php
namespace Ae3\LaravelLogsLayer\app\Exceptions;
use Exception;
class InvalidArgumentException extends Exception
{
}

@ -0,0 +1,8 @@
<?php
namespace Ae3\LaravelLogsLayer\app\Exceptions;
class MissingConfigurationException extends \Exception
{
}

@ -0,0 +1,8 @@
<?php
namespace Ae3\LaravelLogsLayer\app\Exceptions;
class NotImplementedException extends \Exception
{
}

@ -0,0 +1,149 @@
<?php
namespace Ae3\LaravelLogsLayer\app\Handlers;
use DateTimeInterface;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\GuzzleException;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Logger;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\HttpFoundation\Response;
class DiscordHandler extends AbstractProcessingHandler
{
/**
* @var string
*/
private string $webhook;
/**
* @var Client
*/
private Client $client;
/**
* @var int
*/
private int $rateLimitRemaining = 0;
/**
* @var ?int
*/
private ?int $rateLimitReset = null;
/**
* @param string $webhook
* @param int $level
* @param bool $bubble
*/
public function __construct(string $webhook, int $level = Logger::ERROR, bool $bubble = true)
{
$this->webhook = $webhook;
$this->client = new Client();
parent::__construct($level, $bubble);
}
/**
* @param array $record
* @return void
* @throws GuzzleException
*/
protected function write(array $record): void
{
if ($this->rateLimitRemaining === 0 && $this->rateLimitReset !== null) {
$this->waitUntil($this->rateLimitReset);
}
try {
$response = $this->send($record);
} catch (ClientException $exception) {
$response = $exception->getResponse();
if ($response->getStatusCode() !== Response::HTTP_TOO_MANY_REQUESTS) {
throw $exception;
}
$retryAfter = $response->getHeaderLine('Retry-After');
$this->wait((int)$retryAfter);
$this->send($record);
}
$this->rateLimitRemaining = (int)$response->getHeaderLine('X-RateLimit-Remaining');
$this->rateLimitReset = (int)$response->getHeaderLine('X-RateLimit-Reset');
}
/**
* @param array $record
* @return ResponseInterface
* @throws GuzzleException
*/
private function send(array $record): ResponseInterface
{
return $this->client->request('POST', $this->webhook, [
'headers' => [
'Content-Type' => 'application/json'
],
'json' => $this->formatMessage($record),
]);
}
/**
* @param array $record
* @return array[]
*/
private function formatMessage(array $record): array
{
return [
'embeds' => $this->formatEmbeds($record),
];
}
/**
* @param array $record
* @return array[]
*/
private function formatEmbeds(array $record): array
{
$fields = [];
foreach ($record['context'] as $key => $value) {
$value = is_array($value) ? json_encode($value, JSON_PRETTY_PRINT) : (string)$value;
$fields[] = [
'name' => $key,
'value' => $value,
'inline' => true,
];
}
return [
[
'title' => $record['message'],
'timestamp' => $record['datetime']->format(DateTimeInterface::ATOM),
'fields' => $fields,
'footer' => [
'text' => 'level.' . $record['level_name'],
],
]
];
}
/**
* @param int $microseconds
* @return void
*/
private function wait(int $microseconds): void
{
usleep($microseconds);
}
/**
* @param int $timestamp
* @return void
*/
private function waitUntil(int $timestamp): void
{
time_sleep_until($timestamp);
}
}

@ -0,0 +1,55 @@
<?php
namespace Ae3\LaravelLogsLayer\app\Loggers;
use Ae3\LaravelLogsLayer\app\Exceptions\MissingConfigurationException;
use Ae3\LaravelLogsLayer\app\Loggers\Contracts\LoggerContract;
use Illuminate\Support\Facades\App;
use Monolog\Logger;
abstract class AbstractLogger implements LoggerContract
{
/**
* @var string[]
*/
protected array $requiredKeys = ['environments'];
/**
* @inheritDoc
*/
public function __invoke(array $config): ?Logger
{
$this->validateConfig($config);
if (!$this->shouldCreateLogger($config)) {
return null;
}
return $this->createLogger($config);
}
/**
* @inheritDoc
*/
public function validateConfig(array $config): void
{
foreach ($this->requiredKeys as $key) {
if (!array_key_exists($key, $config)) {
throw new MissingConfigurationException("Missing configuration key: $key in email channel");
}
}
}
/**
* @inheritDoc
*/
public function shouldCreateLogger(array $config): bool
{
return App::environment(explode(',', $config['environments']));
}
/**
* @inheritDoc
*/
abstract public function createLogger(array $config): Logger;
}

@ -0,0 +1,37 @@
<?php
namespace Ae3\LaravelLogsLayer\app\Loggers\Contracts;
use Ae3\LaravelLogsLayer\app\Exceptions\MissingConfigurationException;
use Monolog\Logger;
interface LoggerContract
{
/**
* @param array $config
* @return null|Logger
* @throws MissingConfigurationException
*/
public function __invoke(array $config): ?Logger;
/**
* @param array $config
* @return bool
*/
public function shouldCreateLogger(array $config): bool;
/**
* @param array $config
* @return void
* @throws MissingConfigurationException
*/
public function validateConfig(array $config): void;
/**
* @param array $config
* @return Logger
*/
public function createLogger(array $config): Logger;
}

@ -0,0 +1,41 @@
<?php
namespace Ae3\LaravelLogsLayer\app\Loggers;
use Ae3\LaravelLogsLayer\app\Exceptions\MissingConfigurationException;
use Ae3\LaravelLogsLayer\app\Handlers\DiscordHandler;
use Ae3\LaravelLogsLayer\app\Loggers\Contracts\LoggerContract;
use Illuminate\Support\Facades\App;
use Monolog\Logger;
class DiscordLogger extends AbstractLogger
{
/**
* @param array $config
* @return void
* @throws MissingConfigurationException
*/
public function validateConfig(array $config): void
{
$requiredKeys = ['webhook', 'environments'];
foreach (array_merge($requiredKeys, $this->requiredKeys) as $key) {
if (!array_key_exists($key, $config)) {
throw new MissingConfigurationException("Missing configuration key: $key in email channel");
}
}
}
/**
* @param array $config
* @return Logger
*/
public function createLogger(array $config): Logger
{
$log = new Logger('discord');
$log->pushHandler(new DiscordHandler($config['webhook'], Logger::DEBUG, $config['bubble'] ?? true));
return $log;
}
}

@ -0,0 +1,79 @@
<?php
namespace Ae3\LaravelLogsLayer\app\Loggers;
use Ae3\LaravelLogsLayer\app\Exceptions\MissingConfigurationException;
use Monolog\Formatter\HtmlFormatter;
use Monolog\Handler\SwiftMailerHandler;
use Monolog\Logger;
use Swift_Mailer;
use Swift_Message;
class EmailLogger extends AbstractLogger
{
/**
* @param array $config
* @return void
* @throws MissingConfigurationException
*/
public function validateConfig(array $config): void
{
$requiredKeys = ['host', 'port', 'email', 'password', 'encryption', 'subject', 'from', 'to'];
foreach (array_merge($requiredKeys, $this->requiredKeys) as $key) {
if (!array_key_exists($key, $config)) {
throw new MissingConfigurationException("Missing configuration key: $key in email channel");
}
}
}
/**
* @param array $config
* @return Logger
*/
public function createLogger(array $config): Logger
{
$handler = new SwiftMailerHandler(
$this->getMailer($config),
$this->getMailerMessage($config),
Logger::DEBUG,
$config['bubble'] ?? true
);
$handler->setFormatter(new HtmlFormatter());
$logger = new Logger('email');
$logger->pushHandler($handler);
return $logger;
}
/**
* @param array $config
* @return Swift_Mailer
*/
private function getMailer(array $config): Swift_Mailer
{
$transport = new \Swift_SmtpTransport($config['host'], $config['port']);
$transport->setUsername($config['email']);
$transport->setPassword($config['password']);
$transport->setEncryption($config['encryption']);
return new Swift_Mailer($transport);
}
/**
* @param array $config
* @return Swift_Message
*/
private function getMailerMessage(array $config): Swift_Message
{
$message = new Swift_Message();
$message->setSubject($config['subject']);
$message->setFrom($config['from']);
$message->setTo($config['to']);
$message->setContentType('text/html');
return $message;
}
}

@ -0,0 +1,38 @@
<?php
namespace Ae3\LaravelLogsLayer\app\Loggers;
use Ae3\LaravelLogsLayer\app\Exceptions\MissingConfigurationException;
use Monolog\Formatter\LogstashFormatter;
use Monolog\Handler\SocketHandler;
use Monolog\Logger;
class LogstashLogger extends AbstractLogger
{
/**
* @param array $config
* @return void
* @throws MissingConfigurationException
*/
public function validateConfig(array $config): void
{
$requiredKeys = ['host', 'port'];
foreach (array_merge($requiredKeys, $this->requiredKeys) as $key) {
if (!array_key_exists($key, $config)) {
throw new MissingConfigurationException("Missing configuration key: $key in email channel");
}
}
}
/**
* @param array $config
* @return Logger
*/
public function createLogger(array $config): Logger
{
$handler = new SocketHandler("tcp://{$config['host']}:{$config['port']}");
$handler->setFormatter(new LogstashFormatter(config('app.name')));
return new Logger('logstash.main', [$handler]);
}
}

@ -0,0 +1,18 @@
<?php
namespace Ae3\LaravelLogsLayer\app\Middlewares;
use Ae3\LaravelLogsLayer\app\Events\GuzzleEventCaptured;
use Closure;
use Psr\Http\Message\RequestInterface;
class GuzzleLoggingMiddleware
{
public function __invoke(callable $handler): Closure
{
return function (RequestInterface $request, array $options) use ($handler) {
event(new GuzzleEventCaptured($request, $options));
return $handler($request, $options);
};
}
}

@ -0,0 +1,48 @@
<?php
namespace Ae3\LaravelLogsLayer\app\Observers;
use Ae3\LaravelLogsLayer\app\Containers\LogDataContainer;
use Ae3\LaravelLogsLayer\app\Events\GuzzleEventCaptured;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\Event;
class LogCaptureObserver
{
/**
* @return void
*/
public static function registerListeners()
{
Event::listen(QueryExecuted::class, [static::class, 'captureExecutedQueries']);
Event::listen(GuzzleEventCaptured::class, [static::class, 'captureGuzzleEvents']);
}
/**
* @param QueryExecuted $query
* @return void
*/
public static function captureExecutedQueries(QueryExecuted $query)
{
$logDataContainer = app(LogDataContainer::class);
$logDataContainer->addCapturedQuery([
'query' => $query->sql,
'bindings' => $query->bindings,
]);
}
public static function captureGuzzleEvents(GuzzleEventCaptured $event)
{
$logDataContainer = app(LogDataContainer::class);
$logDataContainer->addCapturedHttpClientEvent([
'request' => [
'method' => $event->request->getMethod(),
'uri' => $event->request->getUri(),
'headers' => $event->request->getHeaders(),
],
'options' => $event->options,
]);
}
}

@ -0,0 +1,49 @@
<?php
namespace Ae3\LaravelLogsLayer\app\Providers;
use Ae3\LaravelLogsLayer\app\Containers\LogDataContainer;
use Ae3\LaravelLogsLayer\app\Events\GuzzleEventCaptured;
use Ae3\LaravelLogsLayer\app\Listeners\GuzzleEventCapturedListener;
use Ae3\LaravelLogsLayer\app\Observers\LogCaptureObserver;
use Ae3\LaravelLogsLayer\app\Services\AbstractLogService;
use Illuminate\Support\ServiceProvider;
class LogsLayerServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->mergeConfigFrom(__DIR__ . '/../../config/config.php', 'laravel-logs-layer');
$this->app->singleton(LogDataContainer::class, function ($app) {
return new LogDataContainer();
});
LogCaptureObserver::registerListeners();
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__ . '/../../config/config.php' => config_path('laravel-logs-layer.php'),
], 'config');
}
$this->app->when(AbstractLogService::class)
->needs(LogDataContainer::class)
->give(LogDataContainer::class);
}
}

@ -0,0 +1,209 @@
<?php
namespace Ae3\LaravelLogsLayer\app\Services;
use Ae3\LaravelLogsLayer\app\Containers\LogDataContainer;
use Ae3\LaravelLogsLayer\app\DataTransferObjects\ExceptionContextDTO;
use Illuminate\Support\Facades\Log;
use Throwable;
abstract class AbstractLogService implements Contracts\LogServiceInterface
{
/**
* @var LogDataContainer
*/
private LogDataContainer $logDataContainer;
/**
* @param LogDataContainer $logDataContainer
*/
public function __construct(LogDataContainer $logDataContainer)
{
$this->logDataContainer = $logDataContainer;
}
/**
* @return string
*/
abstract protected function getLogChannel(): string;
/**
* O nível de log emergency é o mais alto em termos de gravidade. Ele deve ser usado para situações em que ocorrem erros graves que requerem intervenção imediata, uma vez que podem representar uma interrupção crítica ou completa do funcionamento da aplicação.
* Exemplos incluem falhas graves de infraestrutura, perda de conexão com bancos de dados essenciais, falhas de segurança críticas, entre outros.
* @param string $caller
* @param Throwable $exception
* @param ExceptionContextDTO $contextDto
* @return void
*/
public function emergency(string $caller, Throwable $exception, ExceptionContextDTO $contextDto): void
{
$logData = $this->buildLogData($caller, $exception, $contextDto);
$this->log('emergency', $exception->getMessage(), $logData);
}
/**
* O nível critical é usado para erros críticos que também requerem atenção urgente, embora possam não ser tão catastróficos quanto os eventos de emergência. Esses erros têm um impacto significativo na operação da aplicação e exigem uma investigação e correção imediatas.
* Um exemplo de uso seria quando uma funcionalidade vital da aplicação falha, mas a aplicação ainda consegue continuar operando em um modo limitado.
* @param string $caller
* @param Throwable $exception
* @param ExceptionContextDTO $contextDto
* @return void
*/
public function critical(string $caller, Throwable $exception, ExceptionContextDTO $contextDto): void
{
$logData = $this->buildLogData($caller, $exception, $contextDto);
$this->log('critical', $exception->getMessage(), $logData);
}
/**
* O nível error é um nível de gravidade menor em comparação com os anteriores. Ele é usado para registrar erros que ocorrem na aplicação, mas não são tão críticos a ponto de interromper completamente o funcionamento da aplicação. Erros desse tipo não impedem a aplicação de continuar operando, mas ainda assim precisam ser corrigidos para evitar problemas futuros ou impactos negativos nos usuários.
* Exemplos podem incluir erros de validação de entrada, erros de banco de dados não críticos, falhas de autenticação, entre outros.
* @param string $caller
* @param Throwable $exception
* @param ExceptionContextDTO $contextDto
* @return void
*/
public function error(string $caller, Throwable $exception, ExceptionContextDTO $contextDto): void
{
$logData = $this->buildLogData($caller, $exception, $contextDto);
$this->log('error', $exception->getMessage(), $logData);
}
/**
* O nível de log warning é usado para registrar situações em que ocorrem problemas potenciais ou indesejados que não são erros graves, mas merecem atenção. Warnings indicam que algo não está funcionando exatamente como o esperado, mas a aplicação ainda é capaz de continuar operando.
* Um exemplo pode ser a depreciação de uma funcionalidade que será removida em futuras versões da aplicação.
* @param string $message
* @param array $messageContext
* @return void
*/
public function warning(string $message, array $messageContext = []): void
{
$logData = $this->buildGenericLogData($message, $messageContext);
$this->log('warning', $message, $logData);
}
/**
* O nível notice é usado para registrar informações importantes que não são consideradas problemas. Esse nível é mais informativo e serve para fornecer insights sobre eventos relevantes na aplicação. Ele é usado para indicar eventos significativos que podem ajudar na depuração e monitoramento da aplicação, mas que não são necessariamente problemas.
* @param string $message
* @param array $messageContext
* @return void
*/
public function notice(string $message, array $messageContext = []): void
{
$logData = $this->buildGenericLogData($message, $messageContext);
$this->log('notice', $message, $logData);
}
/**
* O nível info é usado para registrar informações gerais sobre o funcionamento da aplicação. Ele pode ser usado para registrar eventos normais e ações realizadas pela aplicação, permitindo que você acompanhe o fluxo de execução e atividades relevantes.
* @param string $message
* @param array $messageContext
* @return void
*/
public function info(string $message, array $messageContext = []): void
{
$logData = $this->buildGenericLogData($message, $messageContext);
$this->log('info', $message, $logData);
}
/**
* O nível debug é usado para registros de depuração. Ele é usado para registrar informações detalhadas sobre o fluxo de execução da aplicação, variáveis, valores e outras informações úteis para diagnóstico durante o desenvolvimento. Registros de nível debug geralmente são úteis apenas para desenvolvedores e podem ser desativados em ambientes de produção.
* @param string $message
* @param array $messageContext
* @return void
*/
public function debug(string $message, array $messageContext = []): void
{
$logData = $this->buildGenericLogData($message, $messageContext);
$this->log('debug', $message, $logData);
}
/**
* O nível alert é menos comum e muitas vezes não é usado diretamente. Ele é reservado para situações em que uma ação imediata é necessária, mas que não são tão críticas quanto eventos de emergência ou críticos. Normalmente, um evento de nível alert indicaria uma condição que exige atenção, mas não é uma falha crítica que interrompe a aplicação.
* @param string $message
* @param array $messageContext
* @return void
*/
public function alert(string $message, array $messageContext = []): void
{
$logData = $this->buildGenericLogData($message, $messageContext);
$this->log('alert', $message, $logData);
}
/**
* @param string $caller
* @param Throwable $exception
* @param ExceptionContextDTO $contextDto
* @return array
*/
protected function buildLogData(string $caller, Throwable $exception, ExceptionContextDTO $contextDto): array
{
return [
'caller' => $caller,
'status_code' => $exception->getCode(),
'line' => $exception->getLine(),
'file' => $exception->getFile(),
'error_code' => $contextDto->code,
'custom_data' => $this->asPrettyJson($contextDto->custom_data),
'tags' => $contextDto->tags,
'exception' => get_class($exception),
'current_url' => $contextDto->current_url,
'current_user' => $this->asPrettyJson($contextDto->current_user),
'stack_trace' => $exception->getTraceAsString(),
'classes' => $this->asPrettyJson($this->getContextClasses($exception->getTrace(), $contextDto->root_namespace)),
'queries' => $this->asPrettyJson($this->logDataContainer->getCapturedQueries()),
'guzzle' => $this->asPrettyJson($this->logDataContainer->getCapturedHttpClientEvents()),
];
}
/**
* @param string $message
* @param array $context
* @return array
*/
protected function buildGenericLogData(string $message, array $context): array
{
return [
'message' => $message,
'custom_data' => $context,
];
}
/**
* @param array $stack_trace
* @param string $namespace
* @return array
*/
protected function getContextClasses(array $stack_trace, string $namespace): array
{
$context_classes = [];
foreach ($stack_trace as $trace) {
if (isset($trace['class']) && strpos($trace['class'], $namespace) === 0) {
$context_classes[] = $trace['class'];
}
}
return array_unique($context_classes);
}
/**
* @param $data
* @return false|string
*/
protected function asPrettyJson($data)
{
return json_encode($data, JSON_PRETTY_PRINT);
}
/**
* @param string $level
* @param string $message
* @param array $data
* @return void
*/
protected function log(string $level, string $message, array $data): void
{
Log::channel($this->getLogChannel())->$level($message, $data);
$this->logDataContainer->clearCapturedData();
}
}

@ -0,0 +1,68 @@
<?php
namespace Ae3\LaravelLogsLayer\app\Services\Contracts;
use Ae3\LaravelLogsLayer\app\DataTransferObjects\ExceptionContextDTO;
use Throwable;
interface LogServiceInterface
{
/**
* @param string $caller
* @param Throwable $exception
* @param ExceptionContextDTO $contextDto
* @return void
*/
public function emergency(string $caller, Throwable $exception, ExceptionContextDTO $contextDto): void;
/**
* @param string $message
* @param array $messageContext
* @return void
*/
public function alert(string $message, array $messageContext = []): void;
/**
* @param string $caller
* @param Throwable $exception
* @param ExceptionContextDTO $contextDto
* @return void
*/
public function critical(string $caller, Throwable $exception, ExceptionContextDTO $contextDto): void;
/**
* @param string $caller
* @param Throwable $exception
* @param ExceptionContextDTO $contextDto
* @return void
*/
public function error(string $caller, Throwable $exception, ExceptionContextDTO $contextDto): void;
/**
* @param string $message
* @param array $messageContext
* @return void
*/
public function warning(string $message, array $messageContext = []): void;
/**
* @param string $message
* @param array $messageContext
* @return void
*/
public function notice(string $message, array $messageContext = []): void;
/**
* @param string $message
* @param array $messageContext
* @return void
*/
public function info(string $message, array $messageContext = []): void;
/**
* @param string $message
* @param array $messageContext
* @return void
*/
public function debug(string $message, array $messageContext = []): void;
}

@ -0,0 +1,29 @@
<?php
namespace Ae3\LaravelLogsLayer\app\Services;
class DailyLogService extends AbstractLogService
{
/**
* @inheritDoc
*/
protected function getLogChannel(): string
{
return 'daily';
}
/**
* @inheritDoc
*/
protected function log(string $level, string $message, array $data): void
{
$log_message = json_encode($data);
$log_filename = 'laravel-' . now()->format('Y-m-d') . '.log';
$log_file_path = storage_path('logs/' . $log_filename);
// Abra o arquivo para escrita e adicione a mensagem de log
file_put_contents($log_file_path, $log_message . PHP_EOL, FILE_APPEND);
}
}

@ -0,0 +1,37 @@
<?php
namespace Ae3\LaravelLogsLayer\app\Services;
use Ae3\LaravelLogsLayer\app\DataTransferObjects\ExceptionContextDTO;
use Throwable;
class DiscordLogService extends AbstractLogService
{
/**
* @inheritDoc
*/
protected function getLogChannel(): string
{
return 'discord';
}
/**
* @inheritDoc
*/
protected function buildLogData(string $caller, Throwable $exception, ExceptionContextDTO $contextDto): array
{
return [
'caller' => $caller,
'status_code' => $exception->getCode(),
'line' => $exception->getLine(),
'file' => $exception->getFile(),
'error_code' => $contextDto->code,
'custom_data' => $this->asPrettyJson($contextDto->custom_data),
'tags' => $contextDto->tags,
'exception' => get_class($exception),
'current_url' => $contextDto->current_url,
'current_user' => $this->asPrettyJson($contextDto->current_user),
'classes' => $this->asPrettyJson($this->getContextClasses($exception->getTrace(), $contextDto->root_namespace)),
];
}
}

@ -0,0 +1,14 @@
<?php
namespace Ae3\LaravelLogsLayer\app\Services;
class EmailLogService extends AbstractLogService
{
/**
* @inheritDoc
*/
protected function getLogChannel(): string
{
return 'email';
}
}

@ -0,0 +1,14 @@
<?php
namespace Ae3\LaravelLogsLayer\app\Services;
class LogstashLogService extends AbstractLogService
{
/**
* @inheritDoc
*/
protected function getLogChannel(): string
{
return 'logstash';
}
}

@ -0,0 +1,182 @@
<?php
namespace Ae3\LaravelLogsLayer\app\Traits;
use Ae3\LaravelLogsLayer\app\DataTransferObjects\ExceptionContextDTO;
use Ae3\LaravelLogsLayer\app\DataTransferObjects\LoggedExceptionDTO;
use Ae3\LaravelLogsLayer\app\Exceptions\InvalidArgumentException;
use Ae3\LaravelLogsLayer\app\Services\DailyLogService;
use Ae3\LaravelLogsLayer\app\Services\DiscordLogService;
use Ae3\LaravelLogsLayer\app\Services\EmailLogService;
use Ae3\LaravelLogsLayer\app\Services\LogstashLogService;
use Hashids\Hashids;
use Illuminate\Support\Str;
use RuntimeException;
use Throwable;
trait LogTrait
{
/**
* @var array
*/
private array $logServices = [];
/**
* @return array
*/
public function getLogServices(): array
{
return $this->logServices;
}
/**
* @return void
*/
public function initializeLogServices(): void
{
$defaultChannel = config('logging.default');
$stackChannels = config('logging.channels.stack.channels');
if ($defaultChannel === 'logstash' || in_array('logstash', $stackChannels, true)) {
$this->logServices[] = app(LogstashLogService::class);
}
if ($defaultChannel === 'daily' || in_array('daily', $stackChannels, true)) {
$this->logServices[] = app(DailyLogService::class);
}
if ($defaultChannel === 'discord' || in_array('discord', $stackChannels, true)) {
$this->logServices[] = app(DiscordLogService::class);
}
if ($defaultChannel === 'email' || in_array('email', $stackChannels, true)) {
$this->logServices[] = app(EmailLogService::class);
}
if (empty($this->logServices)) {
throw new RuntimeException('No matching log channel found.');
}
}
/**
* @param string $method
* @param string $message
* @param array $customData
* @return void
*/
private function logWithServices(string $method, string $message, array $customData = []): void
{
$this->initializeLogServices();
foreach ($this->logServices as $logService) {
$logService->$method($message, $customData);
}
}
/**
* @param string $message
* @param array $customData
* @return void
*/
public function logInfo(string $message, array $customData = []): void
{
$this->logWithServices('info', $message, $customData);
}
/**
* @param string $message
* @param array $customData
* @return void
*/
public function logNotice(string $message, array $customData = []): void
{
$this->logWithServices('notice', $message, $customData);
}
/**
* @param string $message
* @param array $customData
* @return void
*/
public function logWarning(string $message, array $customData = []): void
{
$this->logWithServices('warning', $message, $customData);
}
/**
* @param string $message
* @param array $customData
* @return void
*/
public function logDebug(string $message, array $customData = []): void
{
$this->logWithServices('debug', $message, $customData);
}
/**
* @param string $message
* @param array $customData
* @return void
*/
public function logAlert(string $message, array $customData = []): void
{
$this->logWithServices('alert', $message, $customData);
}
/**
* @param string $caller
* @param Throwable $exception
* @param string $log_level
* @param array $customData
* @return LoggedExceptionDTO
* @throws InvalidArgumentException
*/
public function logException(string $caller, Throwable $exception, string $log_level = 'error', array $customData = []): LoggedExceptionDTO
{
$validLogLevels = ['emergency', 'critical', 'error'];
if (!in_array($log_level, $validLogLevels, true)) {
throw new InvalidArgumentException('Invalid log level specified');
}
$errorCode = $this->getRandomErrorCode();
$this->initializeLogServices();
foreach ($this->logServices as $logService) {
$logService->$log_level($caller, $exception, ExceptionContextDTO::fromArray([
'code' => $errorCode,
'root_namespace' => $this->getRootNamespaceFromCaller($caller),
'custom_data' => $customData,
'current_url' => request()->fullUrl(),
'current_user' => auth()->user(),
'tags' => [],
]));
}
return LoggedExceptionDTO::fromArray([
'code' => $errorCode,
'message' => __("Error code: :code", ['code' => $errorCode]),
'level' => $log_level,
]);
}
/**
* @return string
*/
private function getRandomErrorCode(): string
{
$microtime = explode(' ', microtime());
$milliseconds = ((int)$microtime[1]) * 1000 + (int)round($microtime[0] * 1000);
return (new Hashids())->encode($milliseconds);
}
/**
* @param string $caller
* @return string
*/
private function getRootNamespaceFromCaller(string $caller): string
{
return Str::before($caller, '\\');
}
}

@ -0,0 +1,15 @@
<?php
/*
|--------------------------------------------------------------------------
| To publish the config files
|--------------------------------------------------------------------------
|
| Execute the command below do publish the config file
| php artisan vendor:publish --provider="Ae3\LogsLayer\app\Providers\LogsLayerServiceProvider" --tag="config"
*/
return [
'logstash' => [
'environments' => env('LOGSTASH_ENVIRONMENTS', 'production')
]
];

@ -0,0 +1,38 @@
<?php
namespace Ae3\LaravelLogsLayer\Tests;
use Ae3\LaravelLogsLayer\app\Providers\LogsLayerServiceProvider;
use \Orchestra\Testbench\TestCase as OrchestraTestCase;
class TestCase extends OrchestraTestCase
{
/**
* @return void
*/
public function setUp(): void
{
parent::setUp();
}
/**
* @param $app
* @return string[]
*/
protected function getPackageProviders($app): array
{
return [
LogsLayerServiceProvider::class,
];
}
/**
* @param $app
* @return void
*/
protected function getEnvironmentSetUp($app)
{
// perform environment setup
}
}

@ -0,0 +1,158 @@
<?php
namespace Ae3\LaravelLogsLayer\Tests\Unit;
use Ae3\LaravelLogsLayer\app\DataTransferObjects\ExceptionContextDTO;
use Ae3\LaravelLogsLayer\app\Exceptions\InvalidArgumentException;
use Ae3\LaravelLogsLayer\app\Services\Contracts\LogServiceInterface;
use Ae3\LaravelLogsLayer\app\Services\DailyLogService;
use Ae3\LaravelLogsLayer\app\Services\DiscordLogService;
use Ae3\LaravelLogsLayer\app\Services\EmailLogService;
use Ae3\LaravelLogsLayer\app\Services\LogstashLogService;
use Ae3\LaravelLogsLayer\app\Traits\LogTrait;
use Ae3\LaravelLogsLayer\Tests\TestCase;
use Exception;
use Mockery;
use ReflectionException;
class LogTraitTest extends TestCase
{
/**
* @return void
*/
public function tearDown(): void
{
parent::tearDown();
}
/**
* @return void
* @throws ReflectionException
*/
public function testLogServiceReturnsLogstashServiceWhenDefaultIsLogstash(): void
{
config(['logging.default' => 'logstash']);
config(['logging.channels.stack.channels' => ['logstash']]);
$traitInstance = $this->getMockForTrait(LogTrait::class);
$traitInstance->initializeLogServices();
$this->assertContainsOnlyInstancesOf(LogstashLogService::class, $traitInstance->getLogServices());
}
/**
* @return void
* @throws ReflectionException
*/
public function testLogServiceReturnsDailyLogServiceWhenDefaultIsDaily(): void
{
config(['logging.default' => 'daily']);
config(['logging.channels.stack.channels' => ['daily']]);
$traitInstance = $this->getMockForTrait(LogTrait::class);
$traitInstance->initializeLogServices();
$this->assertContainsOnlyInstancesOf(DailyLogService::class, $traitInstance->getLogServices());
}
/**
* @return void
* @throws ReflectionException
*/
public function testLogServiceReturnsDiscordLogServiceWhenDefaultIsDaily(): void
{
config(['logging.default' => 'discord']);
config(['logging.channels.stack.channels' => ['discord']]);
$traitInstance = $this->getMockForTrait(LogTrait::class);
$traitInstance->initializeLogServices();
$this->assertContainsOnlyInstancesOf(DiscordLogService::class, $traitInstance->getLogServices());
}
/**
* @return void
* @throws ReflectionException
*/
public function testLogServiceReturnsEmailLogServiceWhenDefaultIsDaily(): void
{
config(['logging.default' => 'email']);
config(['logging.channels.stack.channels' => ['email']]);
$traitInstance = $this->getMockForTrait(LogTrait::class);
$traitInstance->initializeLogServices();
$this->assertContainsOnlyInstancesOf(EmailLogService::class, $traitInstance->getLogServices());
}
/**
* @return void
* @throws InvalidArgumentException
* @throws ReflectionException
*/
public function testLogExceptionWithInvalidLogLevelThrowsInvalidArgumentException(): void
{
config(['logging.default' => 'discord']);
config(['logging.channels.stack.channels' => ['discord']]);
$traitInstance = $this->getMockForTrait(LogTrait::class);
$this->expectException(InvalidArgumentException::class);
$traitInstance->logException('TestCaller', new Exception('Test exception'), 'invalid_log_level');
}
/**
* @return void
* @throws InvalidArgumentException
* @throws ReflectionException
*/
public function testLogExceptionWithValidLogLevel(): void
{
$traitInstance = $this->getMockForTrait(LogTrait::class);
$mockLogService = Mockery::mock(LogServiceInterface::class);
app()->instance(LogServiceInterface::class, $mockLogService);
$mockException = new Exception('Test exception');
$mockLogContextDTO = ExceptionContextDTO::fromArray([
'root_namespace' => 'mocked_namespace',
'tags' => ['tag1', 'tag2'],
'current_user' => (object)['id' => 1, 'name' => 'Tobias'],
'current_url' => 'https://example.com',
'custom_data' => ['key' => 'value'],
]);
$mockLogService->shouldReceive('error')
->withArgs(function ($caller, $exception, $context) use ($mockLogContextDTO) {
return $caller === 'TestCaller'
&& $exception instanceof Exception
&& $context == $mockLogContextDTO;
});
$result = $traitInstance->logException('TestCaller', $mockException, 'error');
$this->assertStringContainsString('Error code:', $result->message);
$this->assertLogFileContains($result->code);
}
/**
* Testa se o arquivo de log contém o conteúdo esperado
*
* @param string $expectedContent
* @return void
*/
private function assertLogFileContains(string $expectedContent): void
{
// Determinando o caminho do arquivo de log
$logFilename = 'laravel-' . now()->format('Y-m-d') . '.log';
$logFilePath = storage_path('logs/' . $logFilename);
// Garantindo que o arquivo de log existe
$this->assertTrue(file_exists($logFilePath), "O arquivo de log não existe: $logFilePath");
// Lendo o conteúdo do arquivo de log
$logContent = file_get_contents($logFilePath);
// Verificando se o conteúdo esperado está presente no arquivo
$this->assertStringContainsString($expectedContent, $logContent);
}
}
Loading…
Cancel
Save