diff --git a/composer.json b/composer.json index 4ccd07211..f271d0643 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "aws/aws-sdk-php": "^3.0", "doctrine/couchdb": "~1.0@dev", "elasticsearch/elasticsearch": "^7 || ^8", + "friendsofphp/php-cs-fixer": "^3.52", "graylog2/gelf-php": "^1.4.2 || ^2.0", "guzzlehttp/guzzle": "^7.4.5", "guzzlehttp/psr7": "^2.2", @@ -45,6 +46,7 @@ "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "google/cloud": "Allow inclusion of additional contextual information on GCP", "rollbar/rollbar": "Allow sending log messages to Rollbar", "ext-mbstring": "Allow to work properly with unicode symbols", "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", diff --git a/src/Monolog/Formatter/GoogleCloudLoggingFormatter.php b/src/Monolog/Formatter/GoogleCloudLoggingFormatter.php index ea555d4de..402352bf7 100644 --- a/src/Monolog/Formatter/GoogleCloudLoggingFormatter.php +++ b/src/Monolog/Formatter/GoogleCloudLoggingFormatter.php @@ -24,6 +24,10 @@ */ final class GoogleCloudLoggingFormatter extends JsonFormatter { + const CONTEXT_HEADER_FORMAT = '/([0-9a-fA-F]{32})(?:\/(\d+))?(?:;o=(\d+))?/'; + + private static ?string $traceID = null; + protected function normalizeRecord(LogRecord $record): array { $normalized = parent::normalizeRecord($record); @@ -32,9 +36,49 @@ protected function normalizeRecord(LogRecord $record): array $normalized['severity'] = $normalized['level_name']; $normalized['time'] = $record->datetime->format(DateTimeInterface::RFC3339_EXTENDED); + // Tag with Trace ID for request attribution + $normalized['logging.googleapis.com/trace'] = $this->getTraceID(); + // Remove keys that are not used by GCP unset($normalized['level'], $normalized['level_name'], $normalized['datetime']); return $normalized; } + + private function getTraceID(): ?string + { + if (empty($this->traceID) && !empty($_SERVER['HTTP_X_CLOUD_TRACE_CONTEXT'])) { + $matched = preg_match( + self::CONTEXT_HEADER_FORMAT, + $_SERVER['HTTP_X_CLOUD_TRACE_CONTEXT'] ?? '', + $matches, + ); + + if (!$matched) { + return null; + } + + $projectID = $this->getProjectID(); + if (empty($projectID)) { + return null; + } + + $this->traceID = 'projects/'.$projectID.'/traces/'.strtolower($matches[1]); + } + + return $this->traceID; + } + + private function getProjectID(): ?string + { + if (isset($_SERVER['GOOGLE_CLOUD_PROJECT'])) { + return $_SERVER['GOOGLE_CLOUD_PROJECT']; + } + + if (class_exists('\Google\Cloud\Core\Compute\Metadata')) { + return (new \Google\Cloud\Core\Compute\Metadata())->getProjectId(); + } + + return null; + } }