diff --git a/docs/manifest.json b/docs/manifest.json index 186fc7cb7e4..a5f26bc4f4a 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -159,6 +159,23 @@ "master" ] }, + { + "id": "logging-bunyan", + "name": "@google-cloud/logging-bunyan", + "defaultService": "logging-bunyan", + "versions": [ + "master" + ] + }, + { + "id": "logging-winston", + "name": "@google-cloud/logging-winston", + "defaultService": "logging-winston", + "versions": [ + "0.1.0", + "master" + ] + }, { "id": "logging", "name": "@google-cloud/logging", @@ -293,4 +310,4 @@ "title": "npm", "href": "https://www.npmjs.com/package/gcloud" } -} +} \ No newline at end of file diff --git a/packages/logging-bunyan/README.md b/packages/logging-bunyan/README.md new file mode 100644 index 00000000000..7fa101183f2 --- /dev/null +++ b/packages/logging-bunyan/README.md @@ -0,0 +1,72 @@ +# @google-cloud/logging-bunyan +> Stackdriver Logging stream for [Bunyan][bunyan] + +This module provides an easy to use, higher-level layer for working with Stackdriver Logging, compatible with Bunyan. Simply use this as a raw stream with your existing Bunyan loggers. + +For lower-level access to the Stackdriver Logging API, see [@google-cloud/logging][@google-cloud/logging]. + +> *This module is experimental and should be used by early adopters. This module uses APIs that may be undocumented and subject to change without notice.* + +``` sh +$ npm install --save @google-cloud/logging-bunyan +``` +``` js +var bunyan = require('bunyan'); +var loggingBunyan = require('@google-cloud/logging-bunyan')(); + +var logger = bunyan.createLogger({ + name: 'my-service', + streams: [ + loggingBunyan.stream('info') + ] +}); + +logger.error('warp nacelles offline'); +logger.info('shields at 99%'); +``` + +## Authentication + +It's incredibly easy to get authenticated and start using Google's APIs. You can set your credentials on a global basis as well as on a per-API basis. See each individual API section below to see how you can auth on a per-API-basis. This is useful if you want to use different accounts for different Google Cloud services. + +### On Google Cloud Platform + +If you are running this client on Google Cloud Platform, we handle authentication for you with no configuration. You just need to make sure that when you [set up the GCE instance][gce-how-to], you add the correct scopes for the APIs you want to access. + +``` js +var loggingBunyan = require('@google-cloud/logging-bunyan')(); +// ...you're good to go! +``` + +### Elsewhere + +If you are not running this client on Google Cloud Platform, you need a Google Developers service account. To create a service account: + +1. Visit the [Google Developers Console][dev-console]. +2. Create a new project or click on an existing project. +3. Navigate to **APIs & auth** > **APIs section** and turn on the following APIs (you may need to enable billing in order to use these services): + * Stackdriver Logging API +4. Navigate to **APIs & auth** > **Credentials** and then: + * If you want to use a new service account key, click on **Create credentials** and select **Service account key**. After the account key is created, you will be prompted to download the JSON key file that the library uses to authenticate your requests. + * If you want to generate a new service account key for an existing service account, click on **Generate new JSON key** and download the JSON key file. + +``` js +var projectId = process.env.GCLOUD_PROJECT; // E.g. 'grape-spaceship-123' + +var loggingBunyan = require('@google-cloud/logging-bunyan')({ + projectId: projectId, + + // The path to your key file: + keyFilename: '/path/to/keyfile.json' + + // Or the contents of the key file: + credentials: require('./path/to/keyfile.json') +}); + +// ...you're good to go! +``` + +[bunyan]: https://github.com/trentm/node-bunyan +[@google-cloud/logging]: https://www.npmjs.com/package/@google-cloud/logging +[gce-how-to]: https://cloud.google.com/compute/docs/authentication#using +[dev-console]: https://console.developers.google.com/project \ No newline at end of file diff --git a/packages/logging-bunyan/package.json b/packages/logging-bunyan/package.json new file mode 100644 index 00000000000..2a949211334 --- /dev/null +++ b/packages/logging-bunyan/package.json @@ -0,0 +1,48 @@ +{ + "name": "@google-cloud/logging-bunyan", + "version": "0.0.0", + "author": "Google Inc.", + "description": "Stackdriver Logging stream for Bunyan", + "main": "./src/index.js", + "files": [ + "src", + "AUTHORS", + "CONTRIBUTORS", + "COPYING" + ], + "repository": "googlecloudplatform/google-cloud-node", + "keywords": [ + "google apis client", + "google api client", + "google apis", + "google api", + "google", + "google cloud platform", + "google cloud", + "cloud", + "google logging", + "logging", + "stackdriver logging", + "stackdriver", + "bunyan stream", + "winston" + ], + "dependencies": { + "@google-cloud/logging": "^0.7.0" + }, + "devDependencies": { + "bunyan": "^1.8.5", + "extend": "^3.0.0", + "mocha": "^3.2.0", + "proxyquire": "^1.7.11" + }, + "scripts": { + "publish-module": "node ../../scripts/publish.js logging-winston", + "test": "mocha test/*.js", + "system-test": "mocha system-test/*.js --no-timeouts --bail" + }, + "license": "Apache-2.0", + "engines": { + "node": ">=0.12.0" + } +} diff --git a/packages/logging-bunyan/src/index.js b/packages/logging-bunyan/src/index.js new file mode 100644 index 00000000000..b2050b14298 --- /dev/null +++ b/packages/logging-bunyan/src/index.js @@ -0,0 +1,149 @@ +/*! + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*! + * @module logging-bunyan + */ + +'use strict'; + +var logging = require('@google-cloud/logging'); + +/** + * Map of Stackdriver logging levels. + * + * @type {object} + * @private + */ +var BUNYAN_TO_STACKDRIVER = { + 60: 'critical', + 50: 'error', + 40: 'warning', + 30: 'info', + 20: 'debug', + 10: 'debug' +}; + +/** + * This module provides support for streaming your Bunyan logs to + * [Stackdriver Logging]{@link https://cloud.google.com/logging}. + * + * If your app is running on Google Cloud Platform, all configuration and + * authentication is handled for you. We also auto-detect the appropriate + * resource descriptor to report the log entries against. + * + * If you are running your application in anther environment, such as locally, + * on-premise, or on another cloud provider, you will need to provide additional + * configuration. + * + * @constructor + * @alias module:logging-bunyan + * + * @param {object} options - [Configuration object](#/docs). Refer to this link + * for authentication information. + * @param {string=} options.logName - The name of the log that will receive + * messages written to this bunyan stream. Default: `bunyan_Log`. + * @param {object=} options.resource - The monitored resource that the log + * stream corresponds to. On Google Cloud Platform, this is detected + * automatically, but you may optionally specify a specific monitored + * resource. For more information, see the + * [official documentation]{@link https://cloud.google.com/logging/docs/api/reference/rest/v2/MonitoredResource} + * + * @example + * var bunyan = require('bunyan'); + * + * var loggingBunyan = require('@google-cloud/logging-bunyan')({ + * projectId: 'grape-spaceship-123', + * keyFilename: '/path/to/keyfile.json', + * resource: { + * type: 'global' + * } + * }); + * + * var logger = bunyan.createLogger({ + * name: 'my-service', + * streams: [ + * loggingBunyan.stream('info') + * ] + * }); + * + */ +function LoggingBunyan(options) { + if (!(this instanceof LoggingBunyan)) { + return new LoggingBunyan(options); + } + + options = options || {}; + + this.logName_ = options.logName || 'bunyan_log'; + this.resource_ = options.resource; + + this.log_ = logging(options).log(this.logName_); +} + +/** + * Convenience method that Builds a bunyan stream object that you can put in + * the bunyan streams list. + * + * @param {string|number} level - A bunyan logging level. Log entries at or + * above this level will be routed to Stackdriver Logging. + * + * @example + * var logger = bunyan.createLogger({ + * name: 'my-service', + * streams: [ + * loggingBunyan.stream('info') + * ] + * }); + */ +LoggingBunyan.prototype.stream = function(level) { + return { + level: level, + type: 'raw', + stream: this + }; +}; + +/** + * Relay a log entry to the logging agent. This is normally called by bunyan. + * + * @param {object} record - Bunyan log record. + * + * @private + */ +LoggingBunyan.prototype.write = function(record) { + if (typeof record === 'string') { + throw new Error( + '@google-cloud/logging-bunyan only works as a raw bunyan stream type.' + ); + } + + var level = BUNYAN_TO_STACKDRIVER[record.level]; + + var entryMetadata = { + resource: this.resource_, + timestamp: record.time + }; + + var entry = this.log_.entry(entryMetadata, record); + + this.log_[level](entry, function() { + // no-op to avoid a promise being returned. + }); +}; + +module.exports = LoggingBunyan; +module.exports.BUNYAN_TO_STACKDRIVER = BUNYAN_TO_STACKDRIVER; diff --git a/packages/logging-bunyan/system-test/logging-bunyan.js b/packages/logging-bunyan/system-test/logging-bunyan.js new file mode 100644 index 00000000000..fa867bcefd9 --- /dev/null +++ b/packages/logging-bunyan/system-test/logging-bunyan.js @@ -0,0 +1,112 @@ +/*! + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var bunyan = require('bunyan'); + +var env = require('../../../system-test/env.js'); + +var logging = require('@google-cloud/logging')(env); +var loggingBunyan = require('../')(env); + +describe('LoggingBunyan', function() { + var WRITE_CONSISTENCY_DELAY_MS = 20000; + + var logger = bunyan.createLogger({ + name: 'google-cloud-node-system-test', + streams: [ + loggingBunyan.stream('info') + ] + }); + + it('should properly write log entries', function(done) { + var timestamp = new Date(); + + var testData = [ + { + args: [ + 'first' + ], + verify: function(entry) { + assert.strictEqual(entry.data.msg, 'first'); + assert.strictEqual(entry.data.pid, process.pid); + } + }, + + { + args: [ + new Error('second') + ], + verify: function(entry) { + assert.strictEqual(entry.data.msg, 'second'); + assert.strictEqual(entry.data.pid, process.pid); + } + }, + ]; + + var earliest = { + args: [ + { + time: timestamp + }, + 'earliest' + ], + verify: function(entry) { + assert.strictEqual(entry.data.msg, 'earliest'); + assert.strictEqual(entry.data.pid, process.pid); + assert.strictEqual( + entry.metadata.timestamp.toString(), + timestamp.toString() + ); + } + }; + + // Forcibly insert a delay to cause 'third' to have a deterministically + // earlier timestamp. + setTimeout(function() { + testData.forEach(function(test) { + logger.info.apply(logger, test.args); + }); + + // `earliest` is sent last, but it should show up as the earliest entry. + logger.info.apply(logger, earliest.args); + + // insert into list as the earliest entry. + testData.unshift(earliest); + }, 10); + + setTimeout(function() { + var log = logging.log('bunyan_log'); + + log.getEntries({ + pageSize: testData.length + }, function(err, entries) { + assert.ifError(err); + assert.strictEqual(entries.length, testData.length); + + // Make sure entries are valid and are in the correct order. + entries.reverse().forEach(function(entry, index) { + var test = testData[index]; + test.verify(entry); + }); + + done(); + }); + }, WRITE_CONSISTENCY_DELAY_MS); + }); +}); diff --git a/packages/logging-bunyan/test/index.js b/packages/logging-bunyan/test/index.js new file mode 100644 index 00000000000..175053b0250 --- /dev/null +++ b/packages/logging-bunyan/test/index.js @@ -0,0 +1,167 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var extend = require('extend'); +var proxyquire = require('proxyquire'); + +describe('logging-bunyan', function() { + var fakeLogInstance = {}; + var fakeLoggingOptions_; + var fakeLogName_; + + function fakeLogging(options) { + fakeLoggingOptions_ = options; + return { + log: function(logName) { + fakeLogName_ = logName; + return fakeLogInstance; + } + }; + } + + var LoggingBunyanCached; + var LoggingBunyan; + var loggingBunyan; + + var OPTIONS = { + logName: 'log-name', + resource: {} + }; + + before(function() { + LoggingBunyan = proxyquire('../src/index.js', { + '@google-cloud/logging': fakeLogging + }); + + LoggingBunyanCached = extend(true, {}, LoggingBunyan); + }); + + beforeEach(function() { + fakeLogInstance = {}; + fakeLoggingOptions_ = null; + fakeLogName_ = null; + + extend(true, LoggingBunyan, LoggingBunyanCached); + loggingBunyan = new LoggingBunyan(OPTIONS); + }); + + describe('instantiation', function() { + it('should create a new instance of LoggingBunyan', function() { + // jshint newcap:false + var loggingBunyan = LoggingBunyan(OPTIONS); + assert(loggingBunyan instanceof LoggingBunyan); + }); + + it('should localize the provided resource', function() { + assert.strictEqual(loggingBunyan.resource_, OPTIONS.resource); + }); + + it('should localize Log instance using provided name', function() { + assert.strictEqual(fakeLoggingOptions_, OPTIONS); + assert.strictEqual(fakeLogName_, OPTIONS.logName); + }); + + it('should localize Log instance using default name', function() { + var optionsWithoutLogName = extend({}, OPTIONS); + delete optionsWithoutLogName.logName; + + new LoggingBunyan(optionsWithoutLogName); + + assert.strictEqual(fakeLoggingOptions_, optionsWithoutLogName); + assert.strictEqual(fakeLogName_, 'bunyan_log'); + }); + }); + + describe('stream', function() { + it('should return a properly formatted object', function() { + var level = 'info'; + var stream = loggingBunyan.stream(level); + + assert.strictEqual(stream.level, level); + assert.strictEqual(stream.type, 'raw'); + assert.strictEqual(stream.stream, loggingBunyan); + }); + }); + + describe('write', function() { + var STACKDRIVER_LEVEL = 'info'; + + var RECORD = { + level: 30, + time: '2012-06-19T21:34:19.906Z' + }; + + beforeEach(function() { + fakeLogInstance.entry = function() {}; + loggingBunyan.log_[STACKDRIVER_LEVEL] = function() {}; + }); + + it('should throw an error if record is a string', function() { + assert.throws(function() { + loggingBunyan.write('string record'); + }, new RegExp( + '@google-cloud/logging-bunyan only works as a raw bunyan stream type.' + )); + }); + + it('should properly create an entry', function(done) { + loggingBunyan.log_.entry = function(entryMetadata, message) { + assert.deepEqual(entryMetadata, { + resource: loggingBunyan.resource_, + timestamp: RECORD.time + }); + assert.strictEqual(message, RECORD); + done(); + }; + + loggingBunyan.write(RECORD); + }); + + it('should write to the correct log', function(done) { + var customLevel = 'custom-level'; + var entry = {}; + + loggingBunyan.log_.entry = function() { + return entry; + }; + + LoggingBunyan.BUNYAN_TO_STACKDRIVER[RECORD.level] = customLevel; + + loggingBunyan.log_[customLevel] = function(entry_) { + assert.strictEqual(entry_, entry); + done(); + }; + + loggingBunyan.write(RECORD); + }); + }); + + describe('BUNYAN_TO_STACKDRIVER', function() { + it('should correctly map to Stackdriver Logging levels', function() { + assert.deepEqual(LoggingBunyan.BUNYAN_TO_STACKDRIVER, { + 60: 'critical', + 50: 'error', + 40: 'warning', + 30: 'info', + 20: 'debug', + 10: 'debug' + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/logging-winston/README.md b/packages/logging-winston/README.md index 4c8e93b89e6..95041ae8dce 100644 --- a/packages/logging-winston/README.md +++ b/packages/logging-winston/README.md @@ -31,9 +31,9 @@ winston.verbose('sheilds at 99%'); It's incredibly easy to get authenticated and start using Google's APIs. You can set your credentials on a global basis as well as on a per-API basis. See each individual API section below to see how you can auth on a per-API-basis. This is useful if you want to use different accounts for different Google Cloud services. -### On Google Compute Engine +### On Google Cloud Platform -If you are running this client on Google Compute Engine, we handle authentication for you with no configuration. You just need to make sure that when you [set up the GCE instance][gce-how-to], you add the correct scopes for the APIs you want to access. +If you are running this client on Google Cloud Platform, we handle authentication for you with no configuration. You just need to make sure that when you [set up the GCE instance][gce-how-to], you add the correct scopes for the APIs you want to access. ``` js var winston = require('winston'); diff --git a/packages/logging-winston/system-test/logging-winston.js b/packages/logging-winston/system-test/logging-winston.js index 17a8dce8e62..4af50f2c750 100644 --- a/packages/logging-winston/system-test/logging-winston.js +++ b/packages/logging-winston/system-test/logging-winston.js @@ -24,12 +24,16 @@ var winston = require('winston'); var env = require('../../../system-test/env.js'); var logging = require('@google-cloud/logging')(env); -var loggingWinston = require('../'); +var LoggingWinston = require('../'); describe('LoggingWinston', function() { var WRITE_CONSISTENCY_DELAY_MS = 20000; - winston.add(loggingWinston, env); + var logger = new winston.Logger({ + transports: [ + new LoggingWinston(env) + ] + }); describe('log', function() { var testTimestamp = new Date(); @@ -69,7 +73,7 @@ describe('LoggingWinston', function() { it('should properly write log entries', function(done) { async.each(testData, function(test, callback) { - winston.info.apply(winston, test.args.concat(callback)); + logger.info.apply(logger, test.args.concat(callback)); }, function(err) { assert.ifError(err); diff --git a/scripts/docs/config.js b/scripts/docs/config.js index 331171d2576..1c793980b9e 100644 --- a/scripts/docs/config.js +++ b/scripts/docs/config.js @@ -59,6 +59,9 @@ module.exports = { logging: { title: 'Google Cloud Logging' }, + 'logging-bunyan': { + skip: true + }, 'logging-winston': { skip: true },