src/ServiceProvider.php
<?php
namespace AG\ElasticApmLaravel;
use AG\ElasticApmLaravel\Collectors\CommandCollector;
use AG\ElasticApmLaravel\Collectors\DBQueryCollector;
use AG\ElasticApmLaravel\Collectors\EventCounter;
use AG\ElasticApmLaravel\Collectors\FrameworkCollector;
use AG\ElasticApmLaravel\Collectors\HttpRequestCollector;
use AG\ElasticApmLaravel\Collectors\JobCollector;
use AG\ElasticApmLaravel\Collectors\RequestStartTime;
use AG\ElasticApmLaravel\Collectors\ScheduledTaskCollector;
use AG\ElasticApmLaravel\Collectors\SpanCollector;
use AG\ElasticApmLaravel\Contracts\VersionResolver;
use AG\ElasticApmLaravel\Middleware\RecordTransaction;
use AG\ElasticApmLaravel\Services\ApmAgentService;
use AG\ElasticApmLaravel\Services\ApmCollectorService;
use Illuminate\Config\Repository;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
use Nipwaayoni\Config;
class ServiceProvider extends BaseServiceProvider
{
public const COLLECTOR_TAG = 'event-collector';
private $source_config_path = __DIR__ . '/../config/elastic-apm-laravel.php';
/**
* Register the package Facade and APM agent.
*/
public function register(): void
{
$this->mergeConfigFrom($this->source_config_path, 'elastic-apm-laravel');
// Always available, even when inactive
$this->registerFacades();
// Create a single representation of the request start time which can be injected
// to other classes.
$this->app->singleton(RequestStartTime::class, function () {
return new RequestStartTime($this->app['request']->server('REQUEST_TIME_FLOAT') ?? microtime(true));
});
$this->registerAgent();
if (!$this->isAgentDisabled()) {
$this->registerCollectors();
}
}
/**
* Add the global transaction middleware
* and default event collectors.
*/
public function boot(): void
{
$this->publishConfig();
if ($this->isAgentDisabled()) {
return;
}
$this->registerMiddleware();
// If not collecting http events, the http middleware will not be executed and an
// Agent will not exist prior to events occurring. Create one here to ensure the
// collectors all register their listeners before any work is done. Unlike the
// FrameWorkCollector, the JobCollector needs an Agent object so it cannot be
// created independently and discovered by the ServiceProvider later.
if (!$this->collectHttpEvents()) {
$this->app->make(Agent::class);
}
}
/**
* Register Facades into the Service Container.
*/
protected function registerFacades(): void
{
$this->app->bind('apm-collector', function ($app) {
return $app->make(ApmCollectorService::class);
});
$this->app->bind('apm-agent', function ($app) {
return $app->make(ApmAgentService::class);
});
}
/**
* Register the APM Agent into the Service Container.
*/
protected function registerAgent(): void
{
$this->app->singleton(EventCounter::class, function () {
$limit = config('elastic-apm-laravel.spans.maxTraceItems', EventCounter::EVENT_LIMIT);
return new EventCounter($limit);
});
$this->app->singleton(Agent::class, function () {
/** @var AgentBuilder $builder */
$builder = $this->app->make(AgentBuilder::class);
return $builder
->withConfig(new Config($this->getAgentConfig()))
->withEnvData(config('elastic-apm-laravel.env.env'))
->withAppConfig($this->app->make(Repository::class))
->withEventCollectors(collect($this->app->tagged(self::COLLECTOR_TAG)))
->build();
});
// Register a callback on terminating to send the events
$this->app->terminating(function (Request $request, Response $response) {
/** @var Agent $agent */
$agent = $this->app->make(Agent::class);
$agent->send();
});
}
/**
* Add the middleware to the very top of the list,
* aiming to have better time measurements.
*/
protected function registerMiddleware(): void
{
$kernel = $this->app->make(Kernel::class);
$kernel->prependMiddleware(RecordTransaction::class);
}
/**
* Register data collectors and start listening for events. Most collectors are
* registered by tagging the abstracts in the service container. The concreate
* implementations are not created during registration.
*
* All collectors which must be created prior to the boot phase should ensure
* they have no dependencies on other services which may not be registered yet.
*
* All tagged collectors will be gathered and given to the Agent when it is created.
*/
protected function registerCollectors(): void
{
if ($this->collectFrameworkEvents()) {
// Force the FrameworkCollector instance to be created and used. While this appears odd,
// the collector instance registers itself to listen for booting events, so that instance
// must be made available for collection later.
$this->app->instance(FrameworkCollector::class, $this->app->make(FrameworkCollector::class));
$this->app->tag(FrameworkCollector::class, self::COLLECTOR_TAG);
}
if (false !== config('elastic-apm-laravel.spans.querylog.enabled')) {
// DB Queries collector
$this->app->tag(DBQueryCollector::class, self::COLLECTOR_TAG);
}
// Http request collector
if ($this->collectHttpEvents()) {
$this->app->tag(HttpRequestCollector::class, self::COLLECTOR_TAG);
} else {
$this->app->tag(CommandCollector::class, self::COLLECTOR_TAG);
$this->app->tag(ScheduledTaskCollector::class, self::COLLECTOR_TAG);
}
// Job collector
$this->app->tag(JobCollector::class, self::COLLECTOR_TAG);
// Collector for manual measurements throughout the app
$this->app->tag(SpanCollector::class, self::COLLECTOR_TAG);
}
private function collectFrameworkEvents(): bool
{
// For cli executions, like queue workers, the application only
// starts once. It doesn't really make sense to measure freamework events.
return !$this->app->runningInConsole();
}
private function collectHttpEvents(): bool
{
return !$this->app->runningInConsole();
}
/**
* Publish the config file.
*/
protected function publishConfig(): void
{
$this->publishes([$this->source_config_path => $this->getConfigPath()], 'config');
}
/**
* Get the config path.
*/
protected function getConfigPath(): string
{
return config_path('elastic-apm-laravel.php');
}
protected function getAgentConfig(): array
{
return array_merge(
[
'defaultServiceName' => config('elastic-apm-laravel.app.appName'),
'frameworkName' => 'Laravel',
'frameworkVersion' => app()->version(),
'enabled' => config('elastic-apm-laravel.active'),
'environment' => config('elastic-apm-laravel.env.environment'),
'logger' => Log::getLogger(),
'logLevel' => config('elastic-apm-laravel.log-level', 'error'),
],
$this->getAppConfig(),
config('elastic-apm-laravel.server'),
config('elastic-apm-laravel.agent')
);
}
protected function getAppConfig(): array
{
$config = config('elastic-apm-laravel.app');
if ($this->app->bound(VersionResolver::class)) {
$config['serviceVersion'] = $this->app->make(VersionResolver::class)->getVersion();
}
// appName is deprecated in favour of serviceVersion
unset($config['appName']);
return $config;
}
private function isAgentDisabled(): bool
{
return false === config('elastic-apm-laravel.active')
|| ($this->app->runningInConsole() && false === config('elastic-apm-laravel.cli.active'));
}
}