Code Monkey home page Code Monkey logo

cwlogs-writable's Introduction

cwlogs-writable

Writable stream for AWS CloudWatch Logs, inspired by bunyan-cloudwatch.

Build Status Coverage Status Dependencies Status Optional Dependencies Status


Features

  • Uses aws-sdk.
  • Can be used anywhere Writable streams are allowed.
  • Allows for recovery from AWS errors.
  • Creates log groups and streams if they do not exist.
  • Filtering of log events by the stream itself.
  • Safe stringification of log events.

API Docs

There are two forms of the API docs:

  • Normal API docs - Use this if you using cwlogs-writable as-is and not customizing/extending it's functionality.
  • Extended API docs - Use this to also view protected methods that you can use to customize/extend cwlogs-writable.

Quick Start

Install the library using NPM into your existing node project:

npm install cwlogs-writable

Create and write to the stream.

var CWLogsWritable = require('cwlogs-writable');

// Make stream name as unique as possible (see "Picking LogStream Names").
var streamName = 'my-log-stream/' + Date.now()
  + '/' + Math.round(Math.random() * 4026531839 + 268435456).toString(16);

var stream = new CWLogsWritable({
  logGroupName: 'my-aws-log-group',
  logStreamName: streamName,

  // Options passed to the AWS.CloudWatchLogs service.
  cloudWatchLogsOptions: {
    // Change the AWS region as needed.
    region: 'us-east-1',

    // Example authenticating using access key.
    accessKeyId: '{AWS-IAM-USER-ACCESS-KEY-ID}',
    secretAccessKey: '{AWS-SECRET-ACCESS-KEY}'
  }
});

stream.write('example-log-message');

Also consider this checklist:

Bunyan Example

var bunyan = require('bunyan');
var CWLogsWritable = require('cwlogs-writable');

// Make stream name as unique as possible (see "Picking LogStream Names").
var streamName = 'my-log-stream/' + Date.now()
  + '/' + Math.round(Math.random() * 4026531839 + 268435456).toString(16);

var logger = bunyan.createLogger({
  name: 'foo',
  streams: [
    {
      level: 'debug',

      // If 'raw' the CloudWatch log event timestamp will
      // be taken from the bunyan JSON (i.e. rec.time).
      type: 'raw',

      stream: new CWLogsWritable({
        logGroupName: 'my-aws-log-group',
        logStreamName: streamName,
        cloudWatchLogsOptions: { /* ... */ }
      })
    }
  ]
});

Picking LogStream Names

In AWS CloudWatch Logs a LogStream represents "a sequence of log events from a single emitter of logs".

The important part is "single emitter", as this implies that log events should not be put into a LogStream concurrently by multiple emitters.

This is enforced by the PutLogEvents API action which requires each call to include a "sequenceToken". That token is changed each time a call is successful, and the new token is used for the next call.

If an emitter provides an incorrect token, the API will respond with an InvalidSequenceTokenException.

To avoid this error, you must pick LogStream names that are unique to the emitter or at least include enough randomness.

// Example generation of LogStream name
var logStreamName = [

  // Environment identifier (e.g. "production")
  process.env.NODE_ENV || 'development',

  // Current UTC date
  new Date().toISOString().substr(0, 10),

  // EC2 instance ID, optionally provided as an env variable
  process.env.EC2_INSTANCE_ID || null,

  // Process ID
  'p' + process.pid,

  // Random hex string (from "10000000" to "ffffffff")
  Math.round(Math.random() * 4026531839 + 268435456).toString(16),

].filter(Boolean).join('/').replace(/[:*]/g, '');

Capturing Log Record Stringification Errors

Before log records are sent to AWS they must be stringified. cwlogs-writable uses safe stringification techniques to handle circular references that would normally cause JSON.stringify to fail.

Other errors thrown during stringification (e.g. one thrown by a property getter) will also be handled if the optional dependency safe-json-stringify is installed.

If it is not installed, cwlogs-writable will catch the error and emit a stringifyError event.

var stream = new CWLogsWritable({ ... });

// Catch errors if safe-json-stringify is not installed
stream.on('stringifyError', function(err, record) {
  console.log('Failed to stringify log entry!', err);

  // You can attempt to manually process the record argument,
  // but be careful as you will may hit the same error.
  //customBadRecordProcessing(record);
});

Recovering from Errors

By default cwlogs-writable will handle the two most common AWS errors, InvalidSequenceTokenException and DataAlreadyAcceptedException, to give your application as much resiliency as possible.

For all other errors, the default behavior of a CWLogsWritable stream is to emit an 'error' event, clear any queued logs, and ignore all further writes to the stream to prevent memory leaks.

To override this behavior you can provide a onError callback that will allow you to recover from these errors.

var CWLogsWritable = require('cwlogs-writable');

function onError(err, logEvents, next) {
  // Use built-in behavior if the error is not
  // from a PutLogEvents action (logEvents will be null).
  if (!logEvents) {
    next(err);
    return;
  }

  // Requeue the log events after a delay,
  // if the queue is small enough.
  if (this.getQueueSize() < 100) {
    setTimeout(function() {
      // Pass the logEvents to the "next" callback
      // so they are added back to the head of the queue.
      next(logEvents);
    }, 2000);
  }

  // Otherwise, log the events to the console
  // and resume streaming.
  else {
    console.error(
      'Failed to send logEvents: ' +
      JSON.stringify(logEvents)
    );

    next();
  }
}

// Make stream name as unique as possible (see "Picking LogStream Names").
var streamName = 'my-log-stream/' + Date.now()
  + '/' + Math.round(Math.random() * 4026531839 + 268435456).toString(16);

var stream = new CWLogsWritable({
  logGroupName: 'my-aws-log-group',
  logStreamName: streamName,
  cloudWatchLogsOptions: { /* ... */ },

  // Pass the onError callback to CWLogsWritable
  onError: onError
});

Custom Handling of InvalidSequenceTokenException AWS Errors

Frequent InvalidSequenceTokenException AWS errors may indicate a problem with the uniqueness of your LogStream name (see Picking LogStream Names).

If you are experiencing throttling on PutLogEvents or DescribeLogStreams actions, you may want to add custom handling of InvalidSequenceTokenException errors.

// Example of changing the logStreamName on
// InvalidSequenceTokenException errors
// to attempt to avoid further collisions.

function getLogStreamName() {
  return 'my-log-stream/' + Date.now()
    + '/' + Math.round(Math.random() * 4026531839 + 268435456).toString(16);
}

function onError(err, logEvents, next) {
  // Change the LogStream name to get a new
  // randomized value, and requeue the log events.
  if (err.code === 'InvalidSequenceTokenException') {
    this.logStreamName = getLogStreamName();
    next(logEvents);
  }

  // Default to built-in behavior.
  else {
    next(err);
  }
}

var stream = new CWLogsWritable({
  logGroupName: 'my-aws-log-group',
  logStreamName: getLogStreamName(),
  cloudWatchLogsOptions: { /* ... */ },

  // Disable the default handling of
  // InvalidSequenceTokenException errors
  // so onError will get them instead.
  retryOnInvalidSequenceToken: false,

  // Pass the onError callback to CWLogsWritable
  onError: onError
});

CWLogsWritable Options

  • logGroupName

    Required
    Type: string

    AWS CloudWatch LogGroup name. It will be created if it doesn't exist.

  • logStreamName

    Required
    Type: string

    AWS CloudWatch LogStream name. It will be created if it doesn't exist.

  • cloudWatchLogsOptions

    Optional
    Type: object
    Default: {}

    Options passed to AWS.CloudWatchLogs service.

  • writeInterval

    Optional
    Type: string | number
    Default: "nextTick"

    Amount of wait time after a Writable#_write call to allow batching of log events. Must be a positive number or "nextTick". If "nextTick", process.nextTick is used. If a number, setTimeout is used.

  • retryableDelay

    Optional
    Type: string | number
    Default: 150

  • retryableMax

    Optional
    Type: number
    Default: 100

    Maximum number of times an AWS error marked as "retryable" will be retried before the error is instead passed to CWLogsWritable#onError.

  • maxBatchCount

    Optional
    Type: number
    Default: 10000

    Maximum number of log events allowed in a single PutLogEvents API call.

  • maxBatchSize

    Optional
    Type: number
    Default: 1048576

    Maximum number of bytes allowed in a single PutLogEvents API call.

  • ignoreDataAlreadyAcceptedException

    Optional
    Type: boolean
    Default: true

    Ignore DataAlreadyAcceptedException errors. This will bypass CWLogsWritable#onError. See cwlogs-writable/issues/10.

  • retryOnInvalidSequenceToken

    Optional
    Type: boolean
    Default: true

    Retry on InvalidSequenceTokenException errors. This will bypass CWLogsWritable#onError. See cwlogs-writable/issues/12.

  • onError

    Optional
    Type: function

    Called when an AWS error is encountered. Overwrites CWLogsWritable#onError method.

  • filterWrite

    Optional
    Type: function

    Filter writes to CWLogsWritable. Overwrites CWLogsWritable#filterWrite method.

  • objectMode

    Optional
    Type: boolean
    Default: true

    Passed to the Writable constructor. See https://nodejs.org/api/stream.html#stream_object_mode.

Change Log

See CHANGELOG.md

License

The MIT License (MIT)

Copyright (c) 2017 Andre Mekkawi <[email protected]>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

cwlogs-writable's People

Contributors

amekkawi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

cwlogs-writable's Issues

Restrict log batches based on chronological order and time span

According to the CloudWatch Logs API, log events in a PutLogEvents API call...

  • Must be in chronological order by their timestamps.
  • Cannot span more than 24 hours.

Proposed changes:

  • Change CWLogsWritable#nextLogBatchSize to stop adding to the batch size if...
    • A queued log event has an older timestamp than the previous log event in the queue.
    • A queued log event has a timestamp that is more than 24 hours ahead of the first log event that would be part of the batch.

InvalidParameterException error when log message is over PutLogEvents limit

The PutLogEvents limit for a log message is 262118 bytes (256 * 1024 - 26).

The library does not handle messages over this limit, which results in an InvalidParameterException AWS error.

Acceptance Criteria

  • If the initial quick estimate in CWLogsWritable#getMessageSize is over the limit, it uses Buffer.byteLength() to get an accurate measurement and returns that instead.
  • Has CWLogsWritable#reduceOversizedMessage protected method that returns null by default, but can be overridden to attempt to reduce the message size to fit.
  • Has CWLogsWritable#event:oversizeLogEvent with the only argument being the stringified log event message.
  • CWLogsWritable#dequeueNextLogBatch drops a log event and emits an oversizeLogEvent event if:
    • getMessageSize returns a size for the message that is over the limit, and...
    • Passing the message through reduceOversizedMessage does not reduce the message to fit within the limit.

Rev version to 1.0.0 (production ready)

The current version of v0.1.0. node modules that are production-ready are >=1.0.0. If this module is production ready, I recommend revving to 1.0.0, to encourage adoption of this module by other nodejs developers.

Uncaught InvalidSequenceTokenException

After including this module in our project, our application keeps crashing due to InvalidSequenceTokenException: The given sequenceToken is invalid..

This was addressed in bunyan-cloudwatch: mirkokiefer/bunyan-cloudwatch#23

Looking at your code, I did not see where the potential for this error is coming from.

Ignore DataAlreadyAcceptedException by default

Related to mirkokiefer/bunyan-cloudwatch/issues/20, I think it might be good to add special handling for this error.

Given the following series of events, duplicate log events may get put into a log stream:

  1. A cwlogs-writable stream is created and is provided an onError handler that will push the log events back to the queue.
  2. 5 log events are written to the cwlogs-writable stream.
  3. The cwlogs-writable stream sends the 5 log events as a single PutLogEvents request.
  4. The PutLogEvents succeeds but the API response is not received due to a network network issue.
  5. The onError handler is called. It pushes the log events back to the head of the queue. Internally the sequence token is also reset so it is fetched again, which is important because it the resent messages will never result in a DataAlreadyAcceptedException.
  6. The cwlogs-writable stream sends the 5 log events as a single PutLogEvents request, and the success response is received.

Acceptance criteria:

  • CWLogsWritable has ignoreDataAlreadyAcceptedException option that defaults to true.
  • PutLogEvents API action response handler ignores DataAlreadyAcceptedException if ignoreDataAlreadyAcceptedException option is true. Otherwise, it follows current behavior.

Retry on ThrottlingException

API calls may receive 400 Bad Request responses with an error type of ThrottlingException if a rate limit is exceeded by the account or stream.

  • Add support for retrying the API calls when these errors are received.
  • Add customizable maximum number of retries.
  • Add customizable retry delay ("nextTick" or ms delay).

Create Log Stream first instead of trying to get next sequence token

Related to #8, it may be better to take a different approach to handling the initial setup before pushing log events to a log stream.

Right now cwlogs-writable does this:

  1. Describe the Log Stream to get the next sequence token
  2. If it doesn't exist, create the Log Stream and go back to step 1

This means 2 DescribeLogStreams requests are sent for Log Streams that do not yet exist, which should be the case if you're using unique enough Log Stream names.

Instead it may be better to do this:

  1. Attempt to create the Log Stream, ignoring failure
  2. Describe the Log Stream to get the next sequence token (unless the create log stream response includes the token)

Retry on NetworkingError by default

Acceptance criteria

  • CWLogsWritable#retryOnNetworkingError property that defaults to true.
  • CWLogsWritable constructor options support an optional retryOnNetworkingError property that will set to CWLogsWritable#retryOnNetworkingError if it is === false.
  • [TBD] CWLogsWritable ignores errors that have a name property that equals "NetworkingError" and CWLogsWritable#retryOnNetworkingError is true. This bypasses the onError call, and acts the same as calling its next argument as next(); (or next(logEvents); if logEvents was passed).

Allow logGroupName and logStreamName to be changed

The CWLogsWritable#logGroupName and CWLogsWritable#logStreamName are currently read-only properties.

However, it would be useful to change these properties after the creation of a CWLogsWritable.

Use Case

An onError handler is called for a InvalidSequenceTokenException, which likely means multiple emitters are concurrently putting log events to the LogStream. Rather than just ignoring the error and retrying, it would be best to also change the logStreamName to prevent further sequence token errors.

Acceptance criteria

  • logGroupName and logStreamName properties can be changed at any time.
  • Changes to those properties take affect on the next PutLogEvents API call.
  • Changing those properties will reset the sequenceToken so it is requested again, and the new log group and log stream will be created if needed.

Wrong attribution for License

Your license is wrong.

Since this is a fork of bunyan-cloudwatch,
and since that software is licensed under MIT under copyright of Mirko Kiefer,
and since this repository is licensed under MIT under copyright of Andre Mekkawi,
it appears your license should be updated to be under copyright of Mirko Kiefer.

writeInterval default setting

Hi Andre,
Not an issue per se but I was wondering how the default setting of the writeInterval as process.nextTick actually works in practice if many log requests are made per second, given that PutLogEvents() is throttled at 5 requests per second per log stream. Looking at the code the ThrottlingException error isn't handled yet so the error would fall through to next() (unless a custom handler is used to reschedule the upload)? Would setting writeInterval to 200mS act as a throttle to the request rate so the 5 request per second limit would never be hit?
Many thanks,
Allan

Retry on InvalidSequenceTokenException by default

Reconsidering #8 after investigation for #10.

Now that the documentation includes a guide on picking log stream names, I think it'll be better if cwlogs-writable retries on InvalidSequenceTokenException errors by default.

Even if a single emitter is putting events to a LogStream, network issues may cause a situation where a InvalidSequenceTokenException error could be returned.

Acceptance criteria

  • CWLogsWritable#retryOnInvalidSequenceTokenException property that defaults to true.
  • CWLogsWritable constructor options support an optional retryOnInvalidSequenceTokenException property that will set to CWLogsWritable#retryOnInvalidSequenceTokenException if it is === false.
  • CWLogsWritable re-queues log events when a PutLogEvents API request responds with InvalidSequenceTokenException and CWLogsWritable#retryOnInvalidSequenceTokenException is true. This bypasses the onError call, and acts the same as calling its next argument as next(logEvents);.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.