Skip to content

Commit

Permalink
Merge of ibuildingsnl PR to address PSR-4 loading issues when using…
Browse files Browse the repository at this point in the history
… monolog v2.

Merge branch 'ibuildingsnl-master'
  • Loading branch information
zsistla committed Mar 15, 2021
2 parents b4a1e8d + b335d3f commit 3c22b60
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 467 deletions.
3 changes: 1 addition & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
],
"require": {
"php": ">=5.3.0",
"monolog/monolog": "^1.24|^2"
"monolog/monolog": "^2"
},
"require-dev": {
"phpunit/phpunit": "^4",
Expand All @@ -21,7 +21,6 @@
"ext-newrelic": "Adds support for viewing logs in context within the New Relic UI"
},
"autoload": {
"exclude-from-classmap": ["/src/api1/", "/src/api2"],
"psr-4": {
"NewRelic\\Monolog\\Enricher\\": "src"
}
Expand Down
65 changes: 65 additions & 0 deletions src/AbstractFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

/**
* Copyright [2019] New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* This file contains the abstract parent of the Formatter class for
* the New Relic Monolog Enricher. This class implements all functionality
* that is compatible with all Monolog API versions
*
* @author New Relic PHP <php-agent@newrelic.com>
*/

namespace NewRelic\Monolog\Enricher;

use Monolog\Formatter\JsonFormatter;
use Monolog\Logger;

/**
* Formats record as a JSON object with transformations necessary for
* ingestion by New Relic Logs
*/
abstract class AbstractFormatter extends JsonFormatter
{
/**
* @param int $batchMode
* @param bool $appendNewline
*/
public function __construct(
$batchMode = self::BATCH_MODE_NEWLINES,
$appendNewline = true
) {
// BATCH_MODE_NEWLINES is required for batch compatibility with New
// Relic log forwarder plugins, which handle batching records. When
// using the New Relic Monolog handler along side a batching handler
// such as the BufferHandler, BATCH_MODE_JSON is required to adhere
// to the New Relic logs API bulk ingest format.
parent::__construct($batchMode, $appendNewline);
}


/**
* Moves New Relic context information from the
* `$data['extra']['newrelic-context']` array to top level of record,
* converts `datetime` object to `timestamp` top level element represented
* as milliseconds since the UNIX epoch, and finally, normalizes the data
*
* @param mixed $data
* @param int $depth
* @return mixed
*/
protected function normalize($data, $depth = 0)
{
if ($depth == 0) {
if (isset($data['extra']['newrelic-context'])) {
$data = array_merge($data, $data['extra']['newrelic-context']);
unset($data['extra']['newrelic-context']);
}
$data['timestamp'] = intval(
$data['datetime']->format('U.u') * 1000
);
}
return parent::normalize($data, $depth);
}
}
165 changes: 165 additions & 0 deletions src/AbstractHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
<?php

/**
* Copyright [2019] New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* This file contains the abstract parent of the Handler class for
* the New Relic Monolog Enricher. This class implements all functionality
* that is compatible with all Monolog API versions
*
* @author New Relic PHP <php-agent@newrelic.com>
*/

namespace NewRelic\Monolog\Enricher;

use Monolog\Formatter\FormatterInterface;
use Monolog\Handler\Curl;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Handler\HandlerInterface;
use Monolog\Handler\MissingExtensionException;
use Monolog\Logger;
use Monolog\Util;

abstract class AbstractHandler extends AbstractProcessingHandler
{
protected $host = null;
protected $endpoint = 'log/v1';
protected $licenseKey;
protected $protocol = 'https://';

/**
* @param string|int $level The minimum logging level to trigger handler
* @param bool $bubble Whether messages should bubble up the stack.
*
* @throws MissingExtensionException If the curl extension is missing
*/
public function __construct($level = Logger::DEBUG, $bubble = true)
{
if (!extension_loaded('curl')) {
throw new MissingExtensionException(
'The curl extension is required to use this Handler'
);
}

$this->licenseKey = ini_get('newrelic.license');
if (!$this->licenseKey) {
$this->licenseKey = "NO_LICENSE_KEY_FOUND";
}

parent::__construct($level, $bubble);
}

/**
* Sets the New Relic license key. Defaults to the New Relic INI's
* value for 'newrelic.license' if available.
*
* @param string $key
*/
public function setLicenseKey($key)
{
$this->licenseKey = $key;
}

/**
* Sets the hostname of the New Relic Logging API. Defaults to the
* US Prod endpoint (log-api.newrelic.com). Another useful value is
* log-api.eu.newrelic.com for the EU production endpoint.
*
* @param string $host
*/
public function setHost($host)
{
$this->host = $host;
}

/**
* Obtains a curl handler initialized to POST to the host specified by
* $this->setHost()
*
* @return resource $ch curl handler
*/
protected function getCurlHandler()
{
$host = is_null($this->host)
? self::getDefaultHost($this->licenseKey)
: $this->host;

$url = "{$this->protocol}{$host}/{$this->endpoint}";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
return $ch;
}

/**
* Augments JSON-formatted data with New Relic license key and other
* necessary headers, and POSTs the log to the New Relic logging
* endpoint via Curl
*
* @param string $data
*/
protected function send($data)
{
$ch = $this->getCurlHandler();

$headers = array(
'Content-Type: application/json',
'X-License-Key: ' . $this->licenseKey
);

curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
Curl\Util::execute($ch, 5, false);
}

/**
* Augments a JSON-formatted array data with New Relic license key
* and other necessary headers, and POSTs the log to the New Relic
* logging endpoint via Curl
*
* @param string $data
*/
protected function sendBatch($data)
{
$ch = $this->getCurlHandler();

$headers = array(
'Content-Type: application/json',
'X-License-Key: ' . $this->licenseKey
);

$postData = '[{"logs":' . $data . '}]';

curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
Curl\Util::execute($ch, 5, false);
}

/**
* Given a licence key, returns the default log API host for that region.
*
* @param string $licenseKey
* @return string
*/
protected static function getDefaultHost($licenseKey)
{
if (!is_string($licenseKey)) {
throw new \InvalidArgumentException(
'Unknown license key of type ' . gettype($licenseKey)
);
}

$matches = array();
if (preg_match('/^([a-z]{2,3})[0-9]{2}x/', $licenseKey, $matches)) {
$region = ".{$matches[1]}";
} else {
// US licence keys generally don't include region identifiers, so
// we'll default to that.
$region = '';
}

return "log-api$region.newrelic.com";
}
}
87 changes: 32 additions & 55 deletions src/Formatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,77 +4,54 @@
* Copyright [2019] New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* This file contains the abstract parent of the Formatter class for
* the New Relic Monolog Enricher. This class implements all functionality
* that is compatible with all Monolog API versions
* This file contains the Formatter class for the New Relic Monolog Enricher
* on Monolog API v2
*
* This class formats a Monolog record as a JSON object with a compatible
* timestamp, and any New Relic context information moved to the top-level.
* The resulting output is intended to be sent to New Relic Logs via a
* compatible log forwarder with New Relic plugin installed (see this
* project's README for links to available plugins).
*
* @author New Relic PHP <php-agent@newrelic.com>
*/

namespace NewRelic\Monolog\Enricher;

use Monolog\Formatter\JsonFormatter;
use Monolog\Logger;

/**
* Formats record as a JSON object with transformations necessary for
* ingestion by New Relic Logs
*/
abstract class AbstractFormatter extends JsonFormatter
class Formatter extends AbstractFormatter
{
/**
* @param int $batchMode
* @param bool $appendNewline
*/
public function __construct(
$batchMode = self::BATCH_MODE_NEWLINES,
$appendNewline = true
) {
// BATCH_MODE_NEWLINES is required for batch compatibility with New
// Relic log forwarder plugins, which handle batching records. When
// using the New Relic Monolog handler along side a batching handler
// such as the BufferHandler, BATCH_MODE_JSON is required to adhere
// to the New Relic logs API bulk ingest format.
parent::__construct($batchMode, $appendNewline);
}


/**
* Moves New Relic context information from the
* `$data['extra']['newrelic-context']` array to top level of record,
* converts `datetime` object to `timestamp` top level element represented
* as milliseconds since the UNIX epoch, and finally, normalizes the data
* Normalizes each record individually before JSON encoding the complete
* batch of records as a JSON array.
*
* @param mixed $data
* @param int $depth
* @return mixed
* @param array $records
* @return string
*/
protected function normalize($data, $depth = 0)
protected function formatBatchJson(array $records): string
{
if ($depth == 0) {
if (isset($data['extra']['newrelic-context'])) {
$data = array_merge($data, $data['extra']['newrelic-context']);
unset($data['extra']['newrelic-context']);
foreach ($records as $key => $record) {
$normalized = $this->normalize($record);

// Adhere to format of Monolog 2.x JSON format
if (
isset($normalized['context'])
&& $normalized['context'] === []
) {
$normalized['context'] = new \stdClass();
}
if (
isset($normalized['extra'])
&& $normalized['extra'] === []
) {
$normalized['extra'] = new \stdClass();
}
$data['timestamp'] = intval(
$data['datetime']->format('U.u') * 1000
);

$records[$key] = $normalized;
}
return parent::normalize($data, $depth);
return $this->toJson($records, true);
}
}

// phpcs:disable
/*
* This extension to the Monolog framework supports the same PHP versions
* as the New Relic PHP Agent (>=5.3). Older versions of PHP are only
* compatible with Monolog v1, therefore, To accomodate Monolog v2's explicit
* and required type annotations, some overridden methods must be implemented
* both with compatible annotations for v2 and without for v1
*/
if (Logger::API == 2) {
require_once dirname(__FILE__) . '/api2/Formatter.php';
} else {
require_once dirname(__FILE__) . '/api1/Formatter.php';
}
// phpcs:enable

0 comments on commit 3c22b60

Please sign in to comment.