Files
faveo/vendor/bugsnag/bugsnag-laravel/src/BugsnagServiceProvider.php
2023-01-31 13:17:59 +05:30

556 lines
19 KiB
PHP

<?php
namespace Bugsnag\BugsnagLaravel;
use Bugsnag\Breadcrumbs\Breadcrumb;
use Bugsnag\BugsnagLaravel\Middleware\UnhandledState;
use Bugsnag\BugsnagLaravel\Queue\Tracker;
use Bugsnag\BugsnagLaravel\Request\LaravelResolver;
use Bugsnag\Callbacks\CustomUser;
use Bugsnag\Client;
use Bugsnag\Configuration;
use Bugsnag\FeatureFlag;
use Bugsnag\PsrLogger\BugsnagLogger;
use Bugsnag\PsrLogger\MultiLogger as BaseMultiLogger;
use Bugsnag\Report;
use DateTime;
use Illuminate\Auth\GenericUser;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Logging\Log;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Foundation\Application as LaravelApplication;
use Illuminate\Log\LogManager;
use Illuminate\Queue\Events\JobProcessing;
use Illuminate\Queue\QueueManager;
use Illuminate\Routing\Events\RouteMatched;
use Illuminate\Support\ServiceProvider;
use Laravel\Lumen\Application as LumenApplication;
use Monolog\Handler\PsrHandler;
use Monolog\Logger;
use ReflectionClass;
class BugsnagServiceProvider extends ServiceProvider
{
/**
* The package version.
*
* @var string
*/
const VERSION = '2.25.1';
/**
* Boot the service provider.
*
* @return void
*/
public function boot()
{
$this->setupConfig($this->app);
$this->setupEvents($this->app->events, $this->app->config->get('bugsnag'));
$this->setupQueue($this->app->queue);
// Load the Client instance up-front if the OOM bootstrapper has been
// loaded. This avoids the possibility of initialising during an OOM,
// which can take a non-trivial amount of memory
if (class_exists(OomBootstrapper::class, false) && !$this->app->runningUnitTests()) {
$this->app->make('bugsnag');
}
}
/**
* Setup the config.
*
* @param \Illuminate\Contracts\Container\Container $app
*
* @return void
*/
protected function setupConfig(Container $app)
{
$source = realpath($raw = __DIR__.'/../config/bugsnag.php') ?: $raw;
if ($app instanceof LaravelApplication && $app->runningInConsole()) {
$this->publishes([$source => config_path('bugsnag.php')]);
} elseif ($app instanceof LumenApplication) {
$app->configure('bugsnag');
}
$this->mergeConfigFrom($source, 'bugsnag');
}
/**
* Setup the events.
*
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @param array $config
*
* @return void
*/
protected function setupEvents(Dispatcher $events, array $config)
{
if ($this->isSessionTrackingAllowed($config)) {
$events->listen(RouteMatched::class, function ($event) {
$this->app->bugsnag->getSessionTracker()->startSession();
});
}
if (isset($config['query']) && !$config['query']) {
return;
}
$show = isset($config['bindings']) && $config['bindings'];
if (class_exists(QueryExecuted::class)) {
$events->listen(QueryExecuted::class, function (QueryExecuted $query) use ($show) {
$this->app->bugsnag->leaveBreadcrumb(
'Query executed',
Breadcrumb::PROCESS_TYPE,
$this->formatQuery($query->sql, $show ? $query->bindings : [], $query->time, $query->connectionName)
);
});
} else {
$events->listen('illuminate.query', function ($sql, array $bindings, $time, $connection) use ($show) {
$this->app->bugsnag->leaveBreadcrumb(
'Query executed',
Breadcrumb::PROCESS_TYPE,
$this->formatQuery($sql, $show ? $bindings : [], $time, $connection)
);
});
}
}
/**
* Format the query as breadcrumb metadata.
*
* @param string $sql
* @param array $bindings
* @param float $time
* @param string $connection
*
* @return array
*/
protected function formatQuery($sql, array $bindings, $time, $connection)
{
$data = ['sql' => $sql];
foreach ($bindings as $index => $binding) {
$data["binding {$index}"] = $binding;
}
$data['time'] = "{$time}ms";
$data['connection'] = $connection;
return $data;
}
/**
* Setup the queue.
*
* @param \Illuminate\Queue\QueueManager $queue
*
* @return void
*/
protected function setupQueue(QueueManager $queue)
{
$queue->looping(function () {
$this->app->bugsnag->flush();
$this->app->bugsnag->clearBreadcrumbs();
$this->app->make(Tracker::class)->clear();
});
if (!class_exists(JobProcessing::class)) {
return;
}
$queue->before(function (JobProcessing $event) {
$this->app->bugsnag->setFallbackType('Queue');
$job = [
'name' => $event->job->getName(),
'queue' => $event->job->getQueue(),
'attempts' => $event->job->attempts(),
'connection' => $event->connectionName,
];
if (method_exists($event->job, 'resolveName')) {
$job['resolved'] = $event->job->resolveName();
}
$this->app->make(Tracker::class)->set($job);
});
$this->setupQueueForLaravelVapor($queue);
}
/**
* Setup queue events for Laravel Vapor.
*
* This is required because Laravel Vapor's queue system doesn't behave as
* a daemonised queue worker (the 'looping' event never fires) but also
* doesn't behave as a non-daemonised queue worker (our shutdown function
* never fires).
*
* @param QueueManager $queue
*
* @return void
*/
private function setupQueueForLaravelVapor(QueueManager $queue)
{
// ensure we're running on vapor
// this is how vapor-core does it, e.g.:
// https://github.com/laravel/vapor-core/blob/61437221090850ba6e51dce15d0058d362654f9b/src/ConfiguresAssets.php#L16-L19
if (!isset($_ENV['VAPOR_SSM_PATH'])) {
return;
}
// used to keep track of if we're the ones disabling batch sending, so we
// know if we need to re-enable it - if the user disables batch sending
// then they don't want it enabled at all
static $batchSendingWasDisabledByUs = false;
$queue->before(function () use (&$batchSendingWasDisabledByUs) {
// clear breadcrumbs to stop them leaking between jobs
$this->app->bugsnag->clearBreadcrumbs();
// only re-enable batch sending if we're the ones disabling it
// this allows users to disable batch sending entirely
if ($batchSendingWasDisabledByUs) {
$this->app->bugsnag->setBatchSending(true);
}
});
$flush = function () use (&$batchSendingWasDisabledByUs) {
// flush any events created in this job
$this->app->bugsnag->flush();
// disable batch sending so any events after this get sent synchronously
// this is important as exceptions are logged after the 'exceptionOccurred'
// event fires, so the above flush is too early to send them
// these exceptions would get sent after processing the next queued job,
// but we'd still drop the last event when this queue worker stops running
if ($this->app->bugsnag->isBatchSending()) {
$this->app->bugsnag->setBatchSending(false);
$batchSendingWasDisabledByUs = true;
}
};
// added in 5.2.41
if (method_exists($queue, 'after')) {
$queue->after($flush);
}
// added in 5.2.41
if (method_exists($queue, 'exceptionOccurred')) {
$queue->exceptionOccurred($flush);
}
}
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->singleton('bugsnag', function (Container $app) {
$config = $app->config->get('bugsnag');
$client = new Client(new Configuration($config['api_key']), new LaravelResolver($app), $this->getGuzzle($config));
$this->setupCallbacks($client, $app, $config);
$this->setupPaths($client, $app, $config);
$client->setReleaseStage(isset($config['release_stage']) ? $config['release_stage'] : $app->environment());
$client->setHostname(isset($config['hostname']) ? $config['hostname'] : null);
$client->getConfig()->mergeDeviceData(['runtimeVersions' => $this->getRuntimeVersion()]);
$client->setFallbackType($app->runningInConsole() ? 'Console' : 'HTTP');
$client->setAppType(isset($config['app_type']) ? $config['app_type'] : null);
$client->setAppVersion(isset($config['app_version']) ? $config['app_version'] : null);
$client->setBatchSending(isset($config['batch_sending']) ? $config['batch_sending'] : true);
$client->setSendCode(isset($config['send_code']) ? $config['send_code'] : true);
$client->getPipeline()->insertBefore(new UnhandledState(), 'Bugsnag\\Middleware\\SessionData');
$client->setNotifier([
'name' => 'Bugsnag Laravel',
'version' => static::VERSION,
'url' => 'https://github.com/bugsnag/bugsnag-laravel',
]);
if (isset($config['notify_release_stages']) && is_array($config['notify_release_stages'])) {
$client->setNotifyReleaseStages($config['notify_release_stages']);
}
if (isset($config['filters']) && is_array($config['filters'])) {
$client->setFilters($config['filters']);
}
if (isset($config['endpoint'])) {
$client->setNotifyEndpoint($config['endpoint']);
}
if ($this->isSessionTrackingAllowed($config)) {
$endpoint = isset($config['session_endpoint']) ? $config['session_endpoint'] : null;
$this->setupSessionTracking($client, $endpoint, $this->app->events);
}
if (isset($config['build_endpoint'])) {
$client->setBuildEndpoint($config['build_endpoint']);
}
if (array_key_exists('memory_limit_increase', $config)) {
$client->setMemoryLimitIncrease($config['memory_limit_increase']);
}
if (isset($config['discard_classes']) && is_array($config['discard_classes'])) {
$client->setDiscardClasses($config['discard_classes']);
}
if (isset($config['redacted_keys']) && is_array($config['redacted_keys'])) {
$client->setRedactedKeys($config['redacted_keys']);
}
if (isset($config['feature_flags']) && is_array($config['feature_flags']) && $config['feature_flags'] !== []) {
$featureFlags = [];
foreach ($config['feature_flags'] as $flag) {
if (!is_array($flag) || !array_key_exists('name', $flag)) {
continue;
}
if (array_key_exists('variant', $flag)) {
$featureFlags[] = new FeatureFlag($flag['name'], $flag['variant']);
} else {
$featureFlags[] = new FeatureFlag($flag['name']);
}
}
$client->addFeatureFlags($featureFlags);
}
if (isset($config['max_breadcrumbs'])) {
$client->setMaxBreadcrumbs($config['max_breadcrumbs']);
}
return $client;
});
$this->app->singleton('bugsnag.tracker', function () {
return new Tracker();
});
$this->app->singleton('bugsnag.logger', function (Container $app) {
$config = $app->config->get('bugsnag');
$logger = interface_exists(Log::class) ? new LaravelLogger($app['bugsnag'], $app['events']) : new BugsnagLogger($app['bugsnag']);
if (isset($config['logger_notify_level'])) {
$logger->setNotifyLevel($config['logger_notify_level']);
}
return $logger;
});
$this->app->singleton('bugsnag.multi', function (Container $app) {
return interface_exists(Log::class) ? new MultiLogger([$app['log'], $app['bugsnag.logger']]) : new BaseMultiLogger([$app['log'], $app['bugsnag.logger']]);
});
if ($this->app['log'] instanceof LogManager) {
$this->app['log']->extend('bugsnag', function (Container $app, array $config) {
$handler = new PsrHandler($app['bugsnag.logger']);
return new Logger('bugsnag', [$handler]);
});
}
$this->app->alias('bugsnag', Client::class);
$this->app->alias('bugsnag.tracker', Tracker::class);
$this->app->alias('bugsnag.logger', interface_exists(Log::class) ? LaravelLogger::class : BugsnagLogger::class);
$this->app->alias('bugsnag.multi', interface_exists(Log::class) ? MultiLogger::class : BaseMultiLogger::class);
}
/**
* Get the guzzle client instance.
*
* @param array $config
*
* @return \GuzzleHttp\ClientInterface
*/
protected function getGuzzle(array $config)
{
// If a 'bugsnag.guzzle' instance exists in the container, use it
if ($this->app->bound('bugsnag.guzzle')) {
return $this->app->make('bugsnag.guzzle');
}
$options = [];
if (isset($config['proxy']) && $config['proxy']) {
if (isset($config['proxy']['http']) && php_sapi_name() != 'cli') {
unset($config['proxy']['http']);
}
$options['proxy'] = $config['proxy'];
}
return Client::makeGuzzle(null, $options);
}
/**
* Setup the callbacks.
*
* @param \Bugsnag\Client $client
* @param \Illuminate\Contracts\Container\Container $app
* @param array $config
*
* @return void
*/
protected function setupCallbacks(Client $client, Container $app, array $config)
{
if (!isset($config['callbacks']) || $config['callbacks']) {
$client->registerDefaultCallbacks();
$client->registerCallback(function (Report $report) use ($app) {
$tracker = $app->make(Tracker::class);
if ($context = $tracker->context()) {
$report->setContext($context);
}
if ($job = $tracker->get()) {
$report->setMetaData(['job' => $job]);
}
});
}
if (!isset($config['user']) || $config['user']) {
$client->registerCallback(new CustomUser(function () use ($app) {
if ($user = $app->auth->user()) {
if (method_exists($user, 'attributesToArray') && is_callable([$user, 'attributesToArray'])) {
return $user->attributesToArray();
}
if ($user instanceof GenericUser) {
$reflection = new ReflectionClass($user);
$property = $reflection->getProperty('attributes');
$property->setAccessible(true);
return $property->getValue($user);
}
}
}));
}
}
/**
* Setup the client paths.
*
* @param \Bugsnag\Client $client
* @param \Illuminate\Contracts\Container\Container $app
* @param array $config
*
* @return void
*/
protected function setupPaths(Client $client, Container $app, array $config)
{
if (isset($config['project_root_regex'])) {
$client->setProjectRootRegex($config['project_root_regex']);
} elseif (isset($config['project_root'])) {
$client->setProjectRoot($config['project_root']);
} else {
$client->setProjectRoot($app->path());
}
if (isset($config['strip_path_regex'])) {
$client->setStripPathRegex($config['strip_path_regex']);
} elseif (isset($config['strip_path'])) {
$client->setStripPath($config['strip_path']);
} else {
$client->setStripPath($app->basePath());
}
}
/**
* Setup session tracking.
*
* @param \Bugsnag\Client $client
* @param string $endpoint
*
* @return void
*/
protected function setupSessionTracking(Client $client, $endpoint, $events)
{
$client->setAutoCaptureSessions(true);
if (!is_null($endpoint)) {
$client->setSessionEndpoint($endpoint);
}
$sessionTracker = $client->getSessionTracker();
$sessionStorage = function ($session = null) {
if (is_null($session)) {
return session('bugsnag-session', []);
} else {
session(['bugsnag-session' => $session]);
}
};
$sessionTracker->setSessionFunction($sessionStorage);
$cache = $this->app->cache;
$genericStorage = function ($key, $value = null) use ($cache) {
if (is_null($value)) {
return $cache->get($key, null);
} else {
$cache->put($key, $value, new DateTime('+ 1 hour'));
}
};
$sessionTracker->setStorageFunction($genericStorage);
}
/**
* Returns the framework name and version to add to the device data.
*
* Attempt to parse a semantic framework version from $app or else return
* the full version string.
* e.g. Lumen: "Lumen (x.x.x) (Laravel Components y.y.*)" => "x.x.x"
*
* @return array
*/
protected function getRuntimeVersion()
{
$version = $this->app->version();
if (preg_match('/(\d+\.\d+\.\d+)/', $version, $versionMatches)) {
$version = $versionMatches[0];
}
return [($this->app instanceof LumenApplication ? 'lumen' : 'laravel') => $version];
}
/**
* Tests whether session tracking can/should be enabled.
*
* @param array $config The configuration array
*
* @return bool true if session tracking should be enabled.
*/
protected function isSessionTrackingAllowed($config)
{
// Session support removed in Lumen 5.3 - only setup automatic session
// tracking if the session function is avaiable
return isset($config['auto_capture_sessions'])
&& $config['auto_capture_sessions']
&& function_exists('session');
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return ['bugsnag', 'bugsnag.tracker', 'bugsnag.logger', 'bugsnag.multi'];
}
}