Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exception: Log events in a single PutLogEvents request must be in chronological order #4

Open
timkelty opened this issue Oct 3, 2019 · 4 comments

Comments

@timkelty
Copy link

timkelty commented Oct 3, 2019

Got the following Aws\CloudWatchLogs\Exception\CloudWatchLogsException:

"Log events in a single PutLogEvents request must be in chronological order."

@timkelty
Copy link
Author

timkelty commented Oct 3, 2019

Similar issue: maxbanton/cwh#70

@kaushik-manish
Copy link

How did you solve this issue? @timkelty

@timkelty
Copy link
Author

timkelty commented Nov 5, 2019

@manish020195 I bailed on this package, and used maxbandon/cwh directly :)

Here's an example of my log compontent (Craft is an instance of Yii):

        'log' => [
            'targets' => [
                function () {
                    $logger = new \Monolog\Logger('craftcms');
                    $logger->pushHandler(new \Monolog\Handler\StreamHandler('php://stderr', \Monolog\Logger::WARNING));

                    if (!Craft::$app->getRequest()->isConsoleRequest) {
                        $logger->pushHandler(new \Monolog\Handler\StreamHandler('php://stdout', \Monolog\Logger::DEBUG));
                    }

                    return Craft::createObject([
                        'class' => \samdark\log\PsrTarget::class,
                        'except' => ['yii\web\HttpException:40*'],
                        'logVars' => [],
                        'logger' => $logger,
                    ]);
                },
                function () {
                    $handler = (new \ReflectionClass(\Maxbanton\Cwh\Handler\CloudWatch::class))->newInstanceArgs([
                        'client' => new \Aws\CloudWatchLogs\CloudWatchLogsClient([
                            'region' => getenv('AWS_DEFAULT_REGION'),
                            'version' => 'latest',
                            'credentials' => [
                                'key' => getenv('AWS_ACCESS_KEY_ID'),
                                'secret' => getenv('AWS_SECRET_ACCESS_KEY'),
                            ]
                        ]),
                        'group' => '/web/' . CRAFT_ENVIRONMENT,
                        'stream' => @file_get_contents("http://instance-data/latest/meta-data/instance-id") ?: gethostname(),
                        'retention' => 14,
                        'batchSize' => 10000,
                        'tags' => [],
                        'level' => \Monolog\Logger::NOTICE,
                        'bubble' => true,
                    ])->setFormatter(new \Monolog\Formatter\JsonFormatter());

                    return Craft::createObject([
                        'class' => \samdark\log\PsrTarget::class,
                        'except' => ['yii\web\HttpException:40*'],
                        'logger' => (new \Monolog\Logger('craftcms'))->pushHandler($handler),
                        'enabled' => CRAFT_ENVIRONMENT !== 'local',
                    ]);
                }
            ]
        ]

This give me stdout stream logging (for docker logs), and AWS cloudwatch logs remotely.

@devarda
Copy link

devarda commented Apr 14, 2020

Here's a working solution:

  1. No need to install this package, you can remove it

  2. Put this in components/AwsLogTarget.php file. This is the PR fix sequence order #5 (fixed version of this composer package):

<?php
namespace app\components;

use yii\log\Target as BaseTarget;
use yii\base\InvalidConfigException;
use Aws\CloudWatchLogs\CloudWatchLogsClient;
use yii\log\Logger;
use yii\helpers\VarDumper;

class AwsLogTarget extends BaseTarget
{
    /**
     * @var string The name of the log group.
     */
    public $logGroup;

    /**
     * @var string The AWS region to use e.g. eu-west-1.
     */
    public $region;

    /**
     * @var string Your AWS access key.
     */
    public $key;

    /**
     * @var string The name of the log stream. When not set, we try to get the ID of your EC2 instance.
     */
    public $logStream;

    /**
     * @var string Your AWS secret.
     */
    public $secret;

    /**
     * @var CloudWatchLogsClient
     */
    private $client;

    /**
     * @var string
     */
    private $sequenceToken;

    /**
     * @inheritdoc
     */
    public function init()
    {
        if (empty($this->logGroup)) {
            throw new InvalidConfigException("A log group must be set.");
        }

        if (empty($this->region)) {
            throw new InvalidConfigException("The AWS region must be set.");
        }

        if (empty($this->logStream)) {
            if (empty($this->key)) {
                $instanceId = @file_get_contents("http://instance-data/latest/meta-data/instance-id");
                if ($instanceId !== false) {
                    $this->logStream = $instanceId;
                } else {
                    throw new InvalidConfigException("Cannot identify instance ID and no log stream name is set.");
                }
            } else {
                throw new InvalidConfigException("No log stream name is set.");
            }
        }

        $params = [
            'region' => $this->region,
            'version' => 'latest',
        ];

        if (!empty($this->key) && !empty($this->secret)) {
            $params['credentials'] = [
                'key' => $this->key,
                'secret' => $this->secret,
            ];
        }

        $this->client = new CloudWatchLogsClient($params);
    }

    /**
     * @inheritdoc
     */
    public function export()
    {
        $this->ensureLogGroupExists();

        $this->refreshSequenceToken();

        $data = [
            'logEvents' => array_map([$this, 'formatMessage'], $this->messages),
            'logGroupName' => $this->logGroup,
            'logStreamName' => $this->logStream,
        ];

        if (!empty($this->sequenceToken)) {
            $data['sequenceToken'] = $this->sequenceToken;
        }
        $logEvents = $data['logEvents'];

        usort($logEvents, function (array $a, array $b) {
            return $a['timestamp'] > $b['timestamp'] ? 1 : -1;
        });
        $data['logEvents'] = $logEvents;

        $response = $this->client->putLogEvents($data);

        $this->sequenceToken = $response->get('nextSequenceToken');
    }

    /**
     * @inheritdoc
     */
    public function formatMessage($message)
    {
        list($text, $level, $category, $timestamp) = $message;
        $level = Logger::getLevelName($level);
        if (!is_string($text)) {
            // exceptions may not be serializable if in the call stack somewhere is a Closure
            if ($text instanceof \Throwable || $text instanceof \Exception) {
                $text = (string) $text;
            } else {
                $text = VarDumper::export($text);
            }
        }
        $traces = [];
        if (isset($message[4])) {
            foreach ($message[4] as $trace) {
                $traces[] = "in {$trace['file']}:{$trace['line']}";
            }
        }

        $prefix = $this->getMessagePrefix($message);

        return [
            'timestamp' => $timestamp*1000,
            'message' => "{$prefix}[$level][$category] $text" . (empty($traces) ? '' : "\n    " . implode("\n    ", $traces))
        ];
    }

    /**
     * Get the sequence token for the selected log stream.
     *
     * @return void
     */
    private function refreshSequenceToken()
    {
        $existingStreams = $this->client->describeLogStreams([
            'logGroupName' => $this->logGroup,
            'logStreamNamePrefix' => $this->logStream,
        ])->get('logStreams');

        $exists = false;

        foreach ($existingStreams as $stream) {
            if ($stream['logStreamName'] === $this->logStream) {
                $exists = true;
                if (isset($stream['uploadSequenceToken'])) {
                    $this->sequenceToken = $stream['uploadSequenceToken'];
                }
            }
        }

        if (!$exists) {
            $this->client->createLogStream([
                'logGroupName' => $this->logGroup,
                'logStreamName' => $this->logStream,
            ]);
        }
    }

    /**
     * Ensures that the selected log group exists or create it
     *
     * @return void
     */
    private function ensureLogGroupExists()
    {
        $existingGroups = $this->client->describeLogGroups([
            'logGroupNamePrefix' => $this->logGroup,
        ])->get('logGroups');

        $exists = false;

        foreach ($existingGroups as $group) {
            if ($group['logGroupName'] === $this->logGroup) {
                $exists = true;
            }
        }

        if (!$exists) {
            $this->client->createLogGroup([
                'logGroupName' => $this->logGroup,
            ]);
        }
    }
}
  1. Go to your log settings. Mine is at config/params.php and add this in log settings. Make sure to adjust the keys and such.:
	'log' => [
			'traceLevel' => YII_DEBUG ? 3 : 0,
			'targets' => [
                [
                    'class' => 'app\components\AwsLogTarget',
                    'region' => 'XXX',
                    'logGroup' => 'XXX',
                    'logStream' => 'XXX', // omit for automatic instance ID
                    'levels' => ['error', 'warning'],
                    'except' => ['yii\web\HttpException:404'],
                    'logVars' => ['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_SERVER'],
                    'key' => 'XXX', // omit for instance role
                    'secret' => 'XXX', // omit for instance role
                ],
				 
			],
		]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

3 participants