diff --git a/README.md b/README.md index cc728ceb996f..dc3908f14bbc 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ This client supports the following Google Cloud Platform services: * [Google BigQuery](#google-bigquery) * [Google Cloud Datastore](#google-cloud-datastore) * [Google Cloud DNS](#google-cloud-dns) +* [Google Cloud Logging](#google-cloud-logging) * [Google Cloud Pub/Sub](#google-cloud-pubsub) * [Google Cloud Storage](#google-cloud-storage) * [Google Compute Engine](#google-compute-engine) @@ -209,6 +210,49 @@ zone.export('/zonefile.zone', function(err) {}); ``` +## Google Cloud Logging + +- [API Documentation][gcloud-logging-docs] +- [Official Documentation][cloud-logging-docs] + +#### Preview + +```js +// Authenticating on a global-basis. You can also authenticate on a per-API- +// basis (see Authentication section above). +var gcloud = require('gcloud')({ + projectId: 'my-project', + keyFilename: '/path/to/keyfile.json' +}); + +var logging = gcloud.logging(); + +// Create a sink using a Bucket as a destination. +var gcs = gcloud.storage(); + +dns.createSink('my-new-sink', { + destination: gcs.bucket('my-sink') +}, function(err, sink) {}); + +// Write a critical entry to a log. +var log = logging.log('compute.googleapis.com'); + +log.critical({ + metadata: { + serviceName: 'compute.googleapis.com' + }, + data: { + delegate: process.env.USER + } +}, function(err) {}); + +// Get indexes from a service like Compute Engine. +var service = logging.service('compute.googleapis.com'); + +service.getIndexes(function(err, indexes) {}); +``` + + ## Google Cloud Pub/Sub - [API Documentation][gcloud-pubsub-docs] @@ -433,6 +477,7 @@ Apache 2.0 - See [COPYING](COPYING) for more information. [gcloud-compute-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/compute [gcloud-datastore-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/datastore [gcloud-dns-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/dns +[gcloud-logging-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/logging [gcloud-pubsub-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/pubsub [gcloud-resource-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/resource [gcloud-search-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/search @@ -460,6 +505,8 @@ Apache 2.0 - See [COPYING](COPYING) for more information. [cloud-dns-docs]: https://cloud.google.com/dns/docs +[cloud-logging-docs]: https://cloud.google.com/logging/docs + [cloud-pubsub-docs]: https://cloud.google.com/pubsub/docs [cloud-resource-docs]: https://cloud.google.com/resource-manager diff --git a/docs/json/master/logging/.gitkeep b/docs/json/master/logging/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docs/site/components/docs/docs-values.js b/docs/site/components/docs/docs-values.js index ace3d321cb56..99de4b0df80b 100644 --- a/docs/site/components/docs/docs-values.js +++ b/docs/site/components/docs/docs-values.js @@ -135,6 +135,25 @@ angular.module('gcloud.docs') ] }, + logging: { + title: 'Logging', + _url: '{baseUrl}/logging', + pages: [ + { + title: 'Log', + url: '/log' + }, + { + title: 'Service', + url: '/service' + }, + { + title: 'Sink', + url: '/sink' + } + ] + }, + pubsub: { title: 'PubSub', _url: '{baseUrl}/pubsub', @@ -240,6 +259,9 @@ angular.module('gcloud.docs') '>=0.20.0': ['compute'], // introduce resource api. - '>=0.22.0': ['resource'] + '>=0.22.0': ['resource'], + + // introduce logging api. + '>=0.25.0': ['logging'] } }); diff --git a/docs/site/components/docs/docs.html b/docs/site/components/docs/docs.html index 187d6aa776a8..9bb55f8b605b 100644 --- a/docs/site/components/docs/docs.html +++ b/docs/site/components/docs/docs.html @@ -44,7 +44,7 @@


-
diff --git a/docs/site/components/docs/logging-overview.html b/docs/site/components/docs/logging-overview.html new file mode 100644 index 000000000000..55a248401bf8 --- /dev/null +++ b/docs/site/components/docs/logging-overview.html @@ -0,0 +1,8 @@ +

Logging Overview

+

+ The gcloud.logging method will return a logging object, allowing you to create sinks, write log entries, and more. +

+

+ To learn more about Logging, see the What is Google Cloud Logging? +

+ diff --git a/lib/common/service-object.js b/lib/common/service-object.js index ae75f3e1a727..35fa3f7119b8 100644 --- a/lib/common/service-object.js +++ b/lib/common/service-object.js @@ -301,7 +301,7 @@ ServiceObject.prototype.request = function(reqOpts, callback) { }) .join('/'); - this.parent.request(reqOpts, callback); + return this.parent.request(reqOpts, callback); }; module.exports = ServiceObject; diff --git a/lib/common/service.js b/lib/common/service.js index 68dccf8b9c88..7515af706bc1 100644 --- a/lib/common/service.js +++ b/lib/common/service.js @@ -84,7 +84,7 @@ Service.prototype.request = function(reqOpts, callback) { // Good: https://.../projects:list .replace(/\/:/g, ':'); - this.makeAuthenticatedRequest(reqOpts, callback); + return this.makeAuthenticatedRequest(reqOpts, callback); }; module.exports = Service; diff --git a/lib/index.js b/lib/index.js index 71644c2918b0..c9df5d7d0ce2 100644 --- a/lib/index.js +++ b/lib/index.js @@ -108,6 +108,34 @@ var apis = { */ dns: require('./dns'), + /** + *

+ * **This is a *Beta* release of Google Cloud Logging for Google Compute + * Engine.** This feature is not covered by any SLA or deprecation policy + * and may be subject to backward-incompatible changes. + *

+ * + * [Google Cloud Logging](https://cloud.google.com/logging/docs) collects and + * stores logs from applications and services on the Google Cloud Platform: + * + * - Export your logs to Google Cloud Storage, Google BigQuery, or Google + * Cloud Pub/Sub. + * - Integrate third-party logs from your virtual machine instances by + * installing the logging agent, `google-fluentd`. + * + * @type {module:logging} + * + * @return {module:logging} + * + * @example + * var gcloud = require('gcloud'); + * var logging = gcloud.logging({ + * projectId: 'grape-spaceship-123', + * keyFilename: '/path/to/keyfile.json' + * }); + */ + logging: require('./logging'), + /** * [Google Cloud Pub/Sub](https://developers.google.com/pubsub/overview) is a * reliable, many-to-many, asynchronous messaging service from Google Cloud diff --git a/lib/logging/index.js b/lib/logging/index.js new file mode 100644 index 000000000000..8c041ac4bd2a --- /dev/null +++ b/lib/logging/index.js @@ -0,0 +1,539 @@ +/*! + * Copyright 2015 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 + */ + +'use strict'; + +var arrify = require('arrify'); +var extend = require('extend'); +var format = require('string-format-obj'); +var is = require('is'); +var nodeutil = require('util'); + +/** + * @type {module:storage/bucket} + * @private + */ +var Bucket = require('../storage/bucket.js'); + +/** + * @type {module:bigquery/dataset} + * @private + */ +var Dataset = require('../bigquery/dataset.js'); + +/** + * @type {module:logging/log} + * @private + */ +var Log = require('./log.js'); + +/** + * @type {module:common/service} + * @private + */ +var Service = require('../common/service.js'); + +/** + * @type {module:logging/service} + * @private + */ +var ServiceClass = require('./service.js'); + +/** + * @type {module:logging/sink} + * @private + */ +var Sink = require('./sink.js'); + +/** + * @type {module:common/streamrouter} + * @private + */ +var streamRouter = require('../common/stream-router.js'); + +/** + * @type {module:pubsub/topic} + * @private + */ +var Topic = require('../pubsub/topic.js'); + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/** + * [Google Cloud Logging](https://cloud.google.com/logging/docs) collects and + * stores logs from applications and services on the Google Cloud Platform: + * + * - Export your logs to Google Cloud Storage, Google BigQuery, or Google + * Cloud Pub/Sub. + * - Integrate third-party logs from your virtual machine instances by + * installing the logging agent, `google-fluentd`. + * + * @alias module:logging + * @constructor + * + * @resource [What is Google Cloud Logging?]{@link https://cloud.google.com/logging/docs} + * @resource [Introduction to the Cloud Logging API]{@link https://cloud.google.com/logging/docs/api} + * + * @param {object} options - [Configuration object](#/docs/?method=gcloud). + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json', + * projectId: 'grape-spaceship-123' + * }); + * + * var logging = gcloud.logging(); + */ +function Logging(options) { + if (!(this instanceof Logging)) { + options = util.normalizeArguments(this, options); + return new Logging(options); + } + + var config = { + baseUrl: 'https://logging.googleapis.com/v1beta3', + scopes: ['https://www.googleapis.com/auth/logging.admin'] + }; + + Service.call(this, config, options); +} + +nodeutil.inherits(Logging, Service); + +// jscs:disable maximumLineLength +/** + * Create a sink. + * + * **This method only works if you are authenticated as yourself, e.g. using the + * gcloud SDK.** + * + * @resource [Sink Overview]{@link https://cloud.google.com/logging/docs/api/ref/rest/v1beta3/projects.sinks} + * @resource [projects.sinks.create API Documentation]{@link https://cloud.google.com/logging/docs/api/ref/rest/v1beta3/projects.sinks/create} + * + * @throws {Error} if a name is not provided. + * @throws {Error} if a config object is not provided. + * + * @param {string} name - Name of the sink. + * @param {object} config - See a + * [Sink resource](https://cloud.google.com/logging/docs/api/ref/rest/v1beta3/projects.logServices.sinks#LogSink). + * @param {module:storage/bucket|module:bigquery/dataset|module:pubsub/topic} config.destination - + * The destination. If a GCS Bucket object is provided, the proper ACL scope + * will be granted to the bucket. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:loggin/sink} callback.sink - The created Sink object. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * var gcs = gcloud.storage(); + * + * var config = { + * destination: gcs.bucket('logging-bucket') + * }; + * + * function callback(err, sink, apiResponse) { + * // `sink` is a Sink object. + * } + * + * logging.createSink('new-sink-name', config, callback); + */ +Logging.prototype.createSink = function(name, config, callback) { + // jscs:enable maximumLineLength + var self = this; + + if (!is.string(name)) { + throw new Error('A sink name must be provided.'); + } + + if (!is.object(config)) { + throw new Error('A sink configuration object must be provided.'); + } + + if (config.destination instanceof Bucket) { + var bucket = config.destination; + + // Set the proper permissions on the bucket. + bucket.acl.owners.addGroup('cloud-logs@google.com', function(err, apiResp) { + if (err) { + callback(err, null, apiResp); + return; + } + + config.destination = 'storage.googleapis.com/' + bucket.name; + + self.createSink(name, config, callback); + }); + + return; + } + + if (config.destination instanceof Dataset) { + var dataset = config.destination; + + config.destination = format('{baseUrl}/projects/{pId}/datasets/{dId}', { + baseUrl: 'bigquery.googleapis.com', + pId: dataset.parent.projectId, + dId: dataset.id + }); + } + + if (config.destination instanceof Topic) { + var topic = config.destination; + + config.destination = format('{baseUrl}/{topicName}', { + baseUrl: 'pubsub.googleapis.com', + topicName: topic.name + }); + } + + this.request({ + method: 'POST', + uri: '/sinks', + json: extend({}, config, { + name: name + }) + }, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + var sink = self.sink(resp.name); + sink.metadata = resp; + + callback(null, sink, resp); + }); +}; + +/** + * List the logs in this project. **Only logs that have entries are listed.** + * + * @resource [projects.logs.list API Documentation]{@link https://cloud.google.com/logging/docs/api/ref/rest/v1beta3/projects.logs/list} + * + * @param {object=} options - Services filtering options. + * @param {boolean} options.autoPaginate - Have pagination handled + * automatically. Default: true. + * @param {number} options.pageSize - Maximum number of logs to return. + * @param {string} options.pageToken - A previously-returned page token + * representing part of the larger set of results to view. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:logging/log[]} callback.logs - Log objects from your project. + * @param {?object} callback.nextQuery - If present, query with this object to + * check for more results. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * logging.getLogs(function(err, logs) { + * // logs is an array of `Log` objects. + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * function callback(err, logs, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * logging.getLogs(nextQuery, callback); + * } + * } + * + * logging.getLogs({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the logs from your project as a readable object stream. + * //- + * logging.getLogs() + * .on('error', console.error) + * .on('data', function(log) { + * // `log` is an `Log` object. + * }) + * .on('end', function() { + * // All logs retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * logging.getLogs() + * .on('data', function(service) { + * this.end(); + * }); + */ +Logging.prototype.getLogs = function(options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + this.request({ + uri: '/logs', + qs: options + }, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var logs = arrify(resp.logs).map(function(log) { + var logInstance = self.log(log.name); + logInstance.metadata = log; + return logInstance; + }); + + callback(null, logs, nextQuery, resp); + }); +}; + +/** + * List the log services that have entries in this project. + * + * @resource [projects.logServices.list API Documentation]{@link https://cloud.google.com/logging/docs/api/ref/rest/v1beta3/projects.logServices/list} + * + * @param {object=} options - Services filtering options. + * @param {boolean} options.autoPaginate - Have pagination handled + * automatically. Default: true. + * @param {number} options.pageSize - Maximum number of services to return. + * @param {string} options.pageToken - A previously-returned page token + * representing part of the larger set of results to view. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:logging/service[]} callback.services - Service objects from + * your project. + * @param {?object} callback.nextQuery - If present, query with this object to + * check for more results. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * logging.getServices(function(err, services) { + * // services is an array of `Service` objects. + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * function callback(err, services, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * logging.getServices(nextQuery, callback); + * } + * } + * + * logging.getServices({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the services from your project as a readable object stream. + * //- + * logging.getServices() + * .on('error', console.error) + * .on('data', function(service) { + * // `service` is an `Service` object. + * }) + * .on('end', function() { + * // All services retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * logging.getServices() + * .on('data', function(service) { + * this.end(); + * }); + */ +Logging.prototype.getServices = function(options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + this.request({ + uri: '/logServices', + qs: options + }, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var services = arrify(resp.logServices).map(function(service) { + var serviceInstance = self.service(service.name); + serviceInstance.metadata = service; + return serviceInstance; + }); + + callback(null, services, nextQuery, resp); + }); +}; + +/*! Developer Documentation + * + * The JSON API doesn't support any query options. We still hold a place for an + * `options` argument so StreamRouter still works... and in case the API ever + * does support it, like all of the other `list` operations do! + * + * @param {object=} options - Query object. + */ +/** + * Get the sinks associated with this project. + * + * @resource [projects.sinks.list API Documentation]{@link https://cloud.google.com/logging/docs/api/ref/rest/v1beta3/projects.sinks/list} + * + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:logging/sink[]} callback.sinks - Sink objects. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * logging.getSinks(function(err, sinks) { + * // sinks is an array of Sink objects. + * }); + * + * //- + * // Get the sinks from your project as a readable object stream. + * //- + * logging.getSinks() + * .on('error', console.error) + * .on('data', function(sink) { + * // `sink` is a Sink object. + * }) + * .on('end', function() { + * // All sinks retrieved. + * }); + */ +Logging.prototype.getSinks = function(options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + this.request({ + uri: '/sinks' + }, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var sinks = arrify(resp.sinks).map(function(sink) { + var sinkInstance = self.sink(sink.name); + sinkInstance.metadata = sink; + return sinkInstance; + }); + + callback(null, sinks, nextQuery, resp); + }); +}; + +/** + * Get a reference to a Cloud Logging log. + * + * @resource [Log Overview]{@link https://cloud.google.com/logging/docs/api/ref/rest/v1beta3/projects.logs} + * + * @param {string} name - Name of the existing log. + * @return {module:logging/log} + * + * @example + * var log = logging.log('my-log'); + */ +Logging.prototype.log = function(name) { + return new Log(this, name); +}; + +/** + * Get a reference to a Cloud Logging service. + * + * @resource [Service Overview]{@link https://cloud.google.com/logging/docs/api/ref/rest/v1beta3/projects.logServices} + * + * @param {string} name - Name of the existing service. + * @return {module:logging/service} + * + * @example + * var service = logging.service('compute.googleapis.com'); + */ +Logging.prototype.service = function(name) { + return new ServiceClass(this, name); +}; + +/** + * Get a reference to a Cloud Logging sink. + * + * @resource [Sink Overview]{@link https://cloud.google.com/logging/docs/api/ref/rest/v1beta3/projects.sinks} + * + * @param {string} name - Name of the existing sink. + * @return {module:logging/sink} + * + * @example + * var sink = logging.sink('my-sink'); + */ +Logging.prototype.sink = function(name) { + return new Sink(this, name); +}; + +/*! Developer Documentation + * + * These methods can be used with either a callback or as a readable object + * stream. `streamRouter` is used to add this dual behavior. + */ +streamRouter.extend(Logging, ['getLogs', 'getServices', 'getSinks']); + +module.exports = Logging; diff --git a/lib/logging/log.js b/lib/logging/log.js new file mode 100644 index 000000000000..753949fa562b --- /dev/null +++ b/lib/logging/log.js @@ -0,0 +1,415 @@ +/*! + * Copyright 2015 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/log + */ + +'use strict'; + +var arrify = require('arrify'); +var extend = require('extend'); +var is = require('is'); +var JSONStream = require('JSONStream'); +var nodeutil = require('util'); +var pumpify = require('pumpify'); +var streamEvents = require('stream-events'); +var through = require('through2'); + +/** + * @type {module:common/serviceObject} + * @private + */ +var ServiceObject = require('../common/service-object.js'); + +/** + * A log is a named collection of records or entries, each entry representing a + * timestamped event. Logs can be produced by Google Cloud Platform services, by + * third-party services, or by your applications. For example, the log + * `apache-access` is produced by the Apache Web Server, but the log + * `compute.googleapis.com/activity_log` is produced by Google Compute Engine. + * + * @resource [Introduction to Logs]{@link https://cloud.google.com/logging/docs/api/#logs} + * + * @alias module:logging/log + * @constructor + * + * @param {object} options - [Configuration object](#/docs/?method=gcloud). + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json', + * projectId: 'grape-spaceship-123' + * }); + * + * var logging = gcloud.logging(); + * var log = logging.log('my-log'); + */ +function Log(logging, name) { + var methods = { + /** + * Delete the log. + * + * @resource [projects.logs.delete API Documentation]{@link https://cloud.google.com/logging/docs/api/ref/rest/v1beta3/projects.logs/delete} + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this + * request. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * log.delete(function(err, apiResponse) { + * if (!err) { + * // The log was deleted. + * } + * }); + */ + delete: true + }; + + this.name = name.replace(/^.*logs\//, ''); + this.unformattedName = name; + + ServiceObject.call(this, { + parent: logging, + baseUrl: '/logs', + id: this.name, + methods: methods + }); +} + +nodeutil.inherits(Log, ServiceObject); + +/** + * Return an array of log entries with the desired severity assigned. + * + * @private + * + * @param {object|object[]} entries - Log entries. + * @param {string} severity - The desired severity level. + */ +Log.assignSeverityToEntries_ = function(entries, severity) { + return arrify(entries).map(function(entry) { + return extend(true, {}, entry, { + metadata: { + severity: severity + } + }); + }); +}; + +/** + * Write a log entry with a severity of "ALERT". + * + * This is a simple wrapper around {module:logging/log#insert}. All arguments + * are the same as documented there. + * + * @example + * log.alert({ + * metadata: { + * serviceName: 'compute.googleapis.com' + * }, + * data: { + * delegate: process.env.USER + * } + * }, function(err, apiResponse) {}); + */ +Log.prototype.alert = function(entry, labels, callback) { + this.insert(Log.assignSeverityToEntries_(entry, 'ALERT'), labels, callback); +}; + +/** + * Write log entries from a streaming object input. + * + * @example + * var logStream = log.createWriteStream(); + * + * logStream.on('finish', function(err) { + * // All entries have been written. + * }); + * + * logStream.end({ + * // Log entry. + * }); + */ +Log.prototype.createWriteStream = function() { + var self = this; + var writeStream = streamEvents(pumpify.obj()); + + writeStream.once('writing', function() { + var requestStream = self.request({ + method: 'POST', + uri: '/entries:write' + }); + + requestStream.on('response', function(response) { + writeStream.emit('response', response); + }); + + function formatEntry_(entry, enc, next) { + next(null, self.formatEntry_(entry)); + } + + writeStream.setPipeline([ + through.obj(formatEntry_), + JSONStream.stringify('{"entries":[', ',', ']}'), + requestStream + ]); + }); + + return writeStream; +}; + +/** + * Write a log entry with a severity of "CRITICAL". + * + * This is a simple wrapper around {module:logging/log#insert}. All arguments + * are the same as documented there. + * + * @example + * log.critical({ + * metadata: { + * serviceName: 'compute.googleapis.com' + * }, + * data: { + * delegate: process.env.USER + * } + * }, function(err, apiResponse) {}); + */ +Log.prototype.critical = function(entry, labels, callback) { + var entries = Log.assignSeverityToEntries_(entry, 'CRITICAL'); + this.insert(entries, labels, callback); +}; + +/** + * Write a log entry with a severity of "DEBUG". + * + * This is a simple wrapper around {module:logging/log#insert}. All arguments + * are the same as documented there. + * + * @example + * log.debug({ + * metadata: { + * serviceName: 'compute.googleapis.com' + * }, + * data: { + * delegate: process.env.USER + * } + * }, function(err, apiResponse) {}); + */ +Log.prototype.debug = function(entry, labels, callback) { + this.insert(Log.assignSeverityToEntries_(entry, 'DEBUG'), labels, callback); +}; + +/** + * Write a log entry with a severity of "EMERGENCY". + * + * This is a simple wrapper around {module:logging/log#insert}. All arguments + * are the same as documented there. + * + * @example + * log.emergency({ + * metadata: { + * serviceName: 'compute.googleapis.com' + * }, + * data: { + * delegate: process.env.USER + * } + * }, function(err, apiResponse) {}); + */ +Log.prototype.emergency = function(entry, labels, callback) { + var entries = Log.assignSeverityToEntries_(entry, 'EMERGENCY'); + this.insert(entries, labels, callback); +}; + +/** + * Write a log entry with a severity of "ERROR". + * + * This is a simple wrapper around {module:logging/log#insert}. All arguments + * are the same as documented there. + * + * @example + * log.error({ + * metadata: { + * serviceName: 'compute.googleapis.com' + * }, + * data: { + * delegate: process.env.USER + * } + * }, function(err, apiResponse) {}); + */ +Log.prototype.error = function(entry, labels, callback) { + this.insert(Log.assignSeverityToEntries_(entry, 'ERROR'), labels, callback); +}; + +/** + * Write a log entry with a severity of "INFO". + * + * This is a simple wrapper around {module:logging/log#insert}. All arguments + * are the same as documented there. + * + * @example + * log.info({ + * metadata: { + * serviceName: 'compute.googleapis.com' + * }, + * data: { + * delegate: process.env.USER + * } + * }, function(err, apiResponse) {}); + */ +Log.prototype.info = function(entry, labels, callback) { + this.insert(Log.assignSeverityToEntries_(entry, 'INFO'), labels, callback); +}; + +/** + * Write log entries to Cloud Logging. + * + * @resource [projects.logs.entries.write API Documentation]{@link https://cloud.google.com/logging/docs/api/ref/rest/v1beta3/projects.logs.entries/write} + * + * @param {object|object[]} entry - A log entry, or array of entries, to write. + * See a [LogEntry resource](https://cloud.google.com/logging/docs/api/ref/rest/v1beta3/projects.logs.entries/write#LogEntry). + * @param {object|string} entry.data - The payload for this log entry. (Alias + * for `structPayload` and `textPayload` as described by the API) + * @param {object=} entry.metadata - See a + * [LogEntryMetadata resource](https://cloud.google.com/logging/docs/api/ref/rest/v1beta3/projects.logs.entries/write#LogEntryMetadata). + * @param {object[]=} labels - Labels to set on the log. (Alias for + * `commonLabels` as described by the API) + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * var logEntry = { + * metadata: { + * severity: 'INFO' + * }, + * data: { + * delegate: process.env.USER + * } + * }; + * + * log.insert(logEntry, function(err, apiResponse) { + * if (!err) { + * // The log entry was written. + * } + * }); + * + * //- + * // You may also pass multiple log entries to write. + * //- + * var secondLogEntry = { + * metadata: { + * severity: 'DEBUG', + * timestamp: new Date().toJSON() + * }, + * data: { + * delegate: process.env.USER + * } + * }; + * + * log.insert([ + * logEntry, + * secondLogEntry + * ], function(err, apiResponse) { + * if (!err) { + * // The log entries were written. + * } + * }); + */ +Log.prototype.insert = function(entry, labels, callback) { + if (is.fn(labels)) { + callback = labels; + labels = []; + } + + this.request({ + method: 'POST', + uri: '/entries:write', + json: { + commonLabels: labels, + entries: arrify(entry).map(this.formatEntry_.bind(this)) + } + }, function(err, resp) { + callback(err, resp); + }); +}; + +/** + * Write a log entry with a severity of "NOTICE". + * + * This is a simple wrapper around {module:logging/log#insert}. All arguments + * are the same as documented there. + * + * @example + * log.notice({ + * metadata: { + * serviceName: 'compute.googleapis.com' + * }, + * data: { + * delegate: process.env.USER + * } + * }, function(err, apiResponse) {}); + */ +Log.prototype.notice = function(entry, labels, callback) { + this.insert(Log.assignSeverityToEntries_(entry, 'NOTICE'), labels, callback); +}; + +/** + * Write a log entry with a severity of "WARNING". + * + * This is a simple wrapper around {module:logging/log#insert}. All arguments + * are the same as documented there. + * + * @example + * log.warning({ + * metadata: { + * serviceName: 'compute.googleapis.com' + * }, + * data: { + * delegate: process.env.USER + * } + * }, function(err, apiResponse) {}); + */ +Log.prototype.warning = function(entry, labels, callback) { + this.insert(Log.assignSeverityToEntries_(entry, 'WARNING'), labels, callback); +}; + +/** + * All entries are passed through here. This will clone the object and properly + * format it for the API. + * + * @private + * + * @param {object} entry - An entry object. + */ +Log.prototype.formatEntry_ = function(entry) { + entry = extend(true, {}, entry); + + entry.log = this.name; + + if (is.object(entry.data)) { + entry.structPayload = entry.data; + } else if (is.string(entry.data)) { + entry.textPayload = entry.data; + } + + delete entry.data; + + return entry; +}; + +module.exports = Log; diff --git a/lib/logging/service.js b/lib/logging/service.js new file mode 100644 index 000000000000..9a949f56ba07 --- /dev/null +++ b/lib/logging/service.js @@ -0,0 +1,173 @@ +/*! + * Copyright 2015 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/service + */ + +'use strict'; + +var arrify = require('arrify'); +var extend = require('extend'); +var is = require('is'); +var nodeutil = require('util'); + +/** + * @type {module:common/serviceObject} + * @private + */ +var ServiceObject = require('../common/service-object.js'); + +/** + * @type {module:common/streamrouter} + * @private + */ +var streamRouter = require('../common/stream-router.js'); + +/** + * Log services are Google Cloud Platform services that create log entries for + * projects. Log entries must be associated with a service at the time of + * ingestion. Log entries associated with a Google Cloud Platform service are + * indexed by labels specific to that service. The log services available to a + * project are named `projects/{...}/logServices/{...}`. + * + * @resource [Introduction to Log Services]{@link https://cloud.google.com/logging/docs/api/#log_services} + * + * @alias module:logging/service + * @constructor + * + * @param {object} options - [Configuration object](#/docs/?method=gcloud). + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json', + * projectId: 'grape-spaceship-123' + * }); + * + * var logging = gcloud.logging(); + * var service = logging.service('compute.googleapis.com'); + */ +function Service(logging, name) { + ServiceObject.call(this, { + parent: logging, + baseUrl: '/logServices', + id: name, + methods: { + // Only need `request`. + } + }); + + this.name = name; +} + +nodeutil.inherits(Service, ServiceObject); + +/** + * List the current index values for this service. + * + * @resource [projects.logServices.indexes.list API Documentation]{@link https://cloud.google.com/logging/docs/api/ref/rest/v1beta3/projects.logServices.indexes/list} + * + * @param {object=} options - Service query options. + * @param {boolean} options.autoPaginate - Have pagination handled + * automatically. Default: true. + * @param {number} options.pageSize - Maximum number of indexes to return. + * @param {string} options.pageToken - A previously-returned page token + * representing part of the larger set of results to view. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {object} callback.indexes - The current index values. + * @param {?object} callback.nextQuery - If present, query with this object to + * check for more results. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * service.getIndexes(function(err, indexes) { + * // indexes is an array of results from the API. + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * function callback(err, indexes, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * service.getIndexes(nextQuery, callback); + * } + * } + * + * service.getIndexes({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the indexes from your service as a readable object stream. + * //- + * service.getIndexes() + * .on('error', console.error) + * .on('data', function(index) { + * // `index` is a result from the API. + * }) + * .on('end', function() { + * // All indexes retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * service.getIndexes() + * .on('data', function(index) { + * this.end(); + * }); + */ +Service.prototype.getIndexes = function(options, callback) { + if (is.fn(options)) { + callback = options; + options = {}; + } + + this.request({ + uri: '/indexes', + qs: options + }, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var indexes = arrify(resp.serviceIndexPrefixes); + + callback(null, indexes, nextQuery, resp); + }); +}; + +/*! Developer Documentation + * + * These methods can be used with either a callback or as a readable object + * stream. `streamRouter` is used to add this dual behavior. + */ +streamRouter.extend(Service, ['getIndexes']); + +module.exports = Service; diff --git a/lib/logging/sink.js b/lib/logging/sink.js new file mode 100644 index 000000000000..4fce94ca8aba --- /dev/null +++ b/lib/logging/sink.js @@ -0,0 +1,214 @@ +/*! + * Copyright 2015 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/sink + */ + +'use strict'; + +var extend = require('extend'); +var nodeutil = require('util'); + +/** + * @type {module:common/serviceObject} + * @private + */ +var ServiceObject = require('../common/service-object.js'); + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/*! Developer Documentation + * + * @param {module:logging} logging - The Logging instance. + */ +/** + * A sink is an object that lets you to specify a set of log entries to export + * to a particular destination. Cloud Logging lets you export log entries to + * destinations including Google Cloud Storage buckets (for long term log + * storage), Google BigQuery datasets (for log analysis), Google Pub/Sub (for + * streaming to other applications). + * + * @resource [Introduction to Sinks]{@link https://cloud.google.com/logging/docs/api/#sinks} + * + * @alias module:logging/sink + * @constructor + * + * @param {object} options - [Configuration object](#/docs/?method=gcloud). + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json', + * projectId: 'grape-spaceship-123' + * }); + * + * var logging = gcloud.logging(); + * var sink = logging.sink('my-sink'); + */ +function Sink(logging, name) { + var methods = { + /** + * Create a sink. + * + * **This method only works if you are authenticated as yourself, e.g. using + * the gcloud SDK.** + * + * @param {object} config - See {module:logging/createSink}. + * + * @example + * var config = { + * // ... + * }; + * + * sink.create(config, function(err, sink, apiResponse) { + * if (!err) { + * // The sink was created successfully. + * } + * }); + */ + create: true, + + /** + * Delete the sink. + * + * @resource [projects.sink.delete API Documentation]{@link https://cloud.google.com/logging/docs/api/ref/rest/v1beta3/projects.sinks/delete} + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this + * request. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * sink.delete(function(err, apiResponse) { + * if (!err) { + * // The log was deleted. + * } + * }); + */ + delete: true, + + /** + * Get the sink's metadata. + * + * @resource [Sink Resource]{@link https://cloud.google.com/logging/docs/api/ref/rest/v1beta3/projects.sinks#LogSink} + * @resource [projects.sink.get API Documentation]{@link https://cloud.google.com/logging/docs/api/ref/rest/v1beta3/projects.sinks/get} + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this + * request. + * @param {object} callback.metadata - The sink's metadata. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * sink.getMetadata(function(err, metadata, apiResponse) {}); + */ + getMetadata: true + }; + + ServiceObject.call(this, { + parent: logging, + baseUrl: '/sinks', + id: name, + createMethod: logging.createSink.bind(logging), + methods: methods + }); + + this.name = name; +} + +nodeutil.inherits(Sink, ServiceObject); + +/** + * Set the sink's filter. + * + * This will override any filter that was previously set. + * + * **This method only works if you are authenticated as yourself, e.g. using the + * gcloud SDK.** + * + * @resource [Advanced Filters]{@link https://cloud.google.com/logging/docs/view/advanced_filters} + * + * @param {string} filter - The new filter. + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this + * request. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * sink.setFilter('name:*', function(err, apiResponse) {}); + */ +Sink.prototype.setFilter = function(filter, callback) { + this.setMetadata({ + filter: filter + }, callback); +}; + +/** + * Set the sink's metadata. + * + * **This method only works if you are authenticated as yourself, e.g. using the + * gcloud SDK.** + * + * @resource [Sink Resource]{@link https://cloud.google.com/logging/docs/api/ref/rest/v1beta3/projects.sinks#LogSink} + * @resource [projects.sink.update API Documentation]{@link https://cloud.google.com/logging/docs/api/ref/rest/v1beta3/projects.sinks/update} + * + * @param {object} metadata - See a + * [Sink resource](https://cloud.google.com/logging/docs/api/ref/rest/v1beta3/projects.sinks#LogSink). + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this + * request. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * var metadata = { + * filter: 'name:*' + * }; + * + * sink.setMetadata(metadata, function(err, apiResponse) {}); + */ +Sink.prototype.setMetadata = function(metadata, callback) { + var self = this; + + callback = callback || util.noop; + + this.getMetadata(function(err, currentMetadata, apiResponse) { + if (err) { + callback(err, apiResponse); + return; + } + + self.request({ + uri: '', + method: 'PUT', + json: extend({}, currentMetadata, metadata) + }, function(err, apiResponse) { + if (err) { + callback(err, apiResponse); + return; + } + + self.metadata = apiResponse; + + callback(null, apiResponse); + }); + }); +}; + +module.exports = Sink; diff --git a/package.json b/package.json index d9fc91d82353..480363a6d7e9 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "search" ], "dependencies": { + "JSONStream": "^1.0.4", "arrify": "^1.0.0", "async": "^1.4.2", "concat-stream": "^1.5.0", diff --git a/scripts/docs.sh b/scripts/docs.sh index b88506605d37..f18a267dbea3 100755 --- a/scripts/docs.sh +++ b/scripts/docs.sh @@ -43,6 +43,11 @@ ./node_modules/.bin/dox < lib/datastore/request.js > docs/json/master/datastore/request.json & ./node_modules/.bin/dox < lib/datastore/transaction.js > docs/json/master/datastore/transaction.json & +./node_modules/.bin/dox < lib/logging/index.js > docs/json/master/logging/index.json & +./node_modules/.bin/dox < lib/logging/log.js > docs/json/master/logging/log.json & +./node_modules/.bin/dox < lib/logging/service.js > docs/json/master/logging/service.json & +./node_modules/.bin/dox < lib/logging/sink.js > docs/json/master/logging/sink.json & + ./node_modules/.bin/dox < lib/pubsub/index.js > docs/json/master/pubsub/index.json & ./node_modules/.bin/dox < lib/pubsub/subscription.js > docs/json/master/pubsub/subscription.json & ./node_modules/.bin/dox < lib/pubsub/topic.js > docs/json/master/pubsub/topic.json & diff --git a/system-test/logging.js b/system-test/logging.js new file mode 100644 index 000000000000..7f7a610354ce --- /dev/null +++ b/system-test/logging.js @@ -0,0 +1,374 @@ +/*! + * Copyright 2015 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 async = require('async'); +var extend = require('extend'); +var format = require('string-format-obj'); +var is = require('is'); +var uuid = require('node-uuid'); + +var env = require('./env.js'); +var BigQuery = require('../lib/bigquery/index.js'); +var Logging = require('../lib/logging/index.js'); +var PubSub = require('../lib/pubsub/index.js'); +var Storage = require('../lib/storage/index.js'); +var util = require('../lib/common/util.js'); + +describe('Logging', function() { + var logging = new Logging(env); + var storage = new Storage(env); + var bigQuery = new BigQuery(env); + var pubsub = new PubSub(env); + + // Create the possible destinations for sinks that we will create. + var BUCKET_NAME = generateUniqueName(); + var bucket; + + var DATASET_NAME = generateUniqueName().replace(/-/g, '_'); + var dataset = bigQuery.dataset(DATASET_NAME); + + var TOPIC_NAME = generateUniqueName(); + var topic; + + before(function(done) { + async.parallel([createBucket, createDataset, createTopic], done); + + function createBucket(callback) { + storage.createBucket(BUCKET_NAME, function(err, bucket_) { + if (err) { + callback(err); + return; + } + + bucket = bucket_; + callback(); + }); + } + + function createDataset(callback) { + dataset.create(callback); + } + + function createTopic(callback) { + pubsub.createTopic(TOPIC_NAME, function(err, topic_) { + if (err) { + callback(err); + return; + } + + topic = topic_; + callback(); + }); + } + }); + + after(function(done) { + async.parallel([deleteBucket, deleteDataset, deleteTopic], done); + + function deleteBucket(callback) { + bucket.delete(callback); + } + + function deleteDataset(callback) { + dataset.delete(callback); + } + + function deleteTopic(callback) { + topic.delete(callback); + } + }); + + describe('services', function() { + var service; + + before(function(done) { + logging.getServices({ pageSize: 1 }, function(err, services) { + assert.ifError(err); + service = services[0]; + done(); + }); + }); + + it('should list services', function(done) { + logging.getServices({ pageSize: 1 }, function(err, services) { + assert.ifError(err); + assert.strictEqual(services.length, 1); + done(); + }); + }); + + it('should list services as a stream', function(done) { + logging.getServices({ pageSize: 1 }) + .on('error', done) + .once('data', function() { + this.end(); + done(); + }); + }); + + it('should get indexes', function(done) { + service.getIndexes({ pageSize: 1 }, function(err, indexes) { + assert.ifError(err); + assert.strictEqual(indexes.length, 1); + done(); + }); + }); + + it('should list indexes as a stream', function(done) { + service.getIndexes({ pageSize: 1 }) + .on('error', done) + .on('data', util.noop) + .on('end', done); + }); + }); + + describe('sinks', function() { + var sink; + + before(function(done) { + var name = generateUniqueName(); + + // To create a Sink, auth through the gcloud SDK is required. + var logging = new Logging({ + projectId: env.projectId + }); + + sink = logging.sink(name); + + sink.create({ + destination: bucket + }, function(err, sink, apiResponse) { + assert.ifError(err); + + assert.strictEqual(sink.name, name); + + var destination = 'storage.googleapis.com/' + bucket.name; + assert.strictEqual(apiResponse.destination, destination); + + done(); + }); + }); + + after(function(done) { + sink.delete(done); + }); + + it('should list sinks', function(done) { + logging.getSinks(function(err, sinks) { + assert.ifError(err); + assert(sinks.length > 1); + done(); + }); + }); + + it('should list sinks as a stream', function(done) { + logging.getSinks({ pageSize: 1 }) + .on('error', done) + .once('data', function() { + this.end(); + done(); + }); + }); + + it('should create a sink with a Dataset destination', function(done) { + var name = generateUniqueName(); + + // To create a Sink, auth through the gcloud SDK is required. + var logging = new Logging({ + projectId: env.projectId + }); + + var sink = logging.sink(name); + + sink.create({ + destination: dataset + }, function(err, sink, apiResponse) { + assert.ifError(err); + + var destination = format('{baseUrl}/projects/{pId}/datasets/{dId}', { + baseUrl: 'bigquery.googleapis.com', + pId: dataset.parent.projectId, + dId: dataset.id + }); + + assert.strictEqual(apiResponse.destination, destination); + + sink.delete(done); + }); + }); + + it('should create a sink with a Topic destination', function(done) { + var name = generateUniqueName(); + + // To create a Sink, auth through the gcloud SDK is required. + var logging = new Logging({ + projectId: env.projectId + }); + + var sink = logging.sink(name); + + sink.create({ + destination: topic + }, function(err, sink, apiResponse) { + assert.ifError(err); + + var destination = 'pubsub.googleapis.com/' + topic.name; + assert.strictEqual(apiResponse.destination, destination); + + sink.delete(done); + }); + }); + + describe('metadata', function() { + it('should get metadata', function(done) { + sink.getMetadata(function(err, metadata) { + assert.ifError(err); + assert.strictEqual(is.object(metadata), true); + done(); + }); + }); + + // To update metadata, auth through the gcloud SDK is required. + it('should set metadata', function(done) { + var metadata = { + filter: 'name:*' + }; + + sink.setMetadata(metadata, function(err, apiResponse) { + assert.ifError(err); + assert.strictEqual(apiResponse.filter, metadata.filter); + done(); + }); + }); + + // To update metadata, auth through the gcloud SDK is required. + it('should set a filter', function(done) { + var filter = 'name:*'; + + sink.setFilter(filter, function(err, apiResponse) { + assert.ifError(err); + assert.strictEqual(apiResponse.filter, filter); + done(); + }); + }); + }); + }); + + describe('logs', function() { + var log = logging.log('compute.googleapis.com'); + + var logEntry = { + metadata: { + severity: 'INFO', + serviceName: 'compute.googleapis.com' + }, + data: { + delegate: process.env.USER + } + }; + + it('should list logs', function(done) { + logging.getLogs({ pageSize: 1 }, function(err, logs) { + assert.ifError(err); + assert.strictEqual(logs.length, 1); + done(); + }); + }); + + it('should list logs as a stream', function(done) { + logging.getLogs({ pageSize: 1 }) + .on('error', done) + .once('data', function() { + this.end(); + done(); + }); + }); + + it('should write to a log', function(done) { + log.insert(logEntry, done); + }); + + it('should write to a log with alert helper', function(done) { + var entry = extend({}, logEntry); + delete entry.metadata.severity; + + log.alert(entry, done); + }); + + it('should write to a log with critical helper', function(done) { + var entry = extend({}, logEntry); + delete entry.metadata.severity; + + log.critical(entry, done); + }); + + it('should write to a log with debug helper', function(done) { + var entry = extend({}, logEntry); + delete entry.metadata.severity; + + log.debug(entry, done); + }); + + it('should write to a log with emergency helper', function(done) { + var entry = extend({}, logEntry); + delete entry.metadata.severity; + + log.emergency(entry, done); + }); + + it('should write to a log with error helper', function(done) { + var entry = extend({}, logEntry); + delete entry.metadata.severity; + + log.error(entry, done); + }); + + it('should write to a log with info helper', function(done) { + var entry = extend({}, logEntry); + delete entry.metadata.severity; + + log.info(entry, done); + }); + + it('should write to a log with notice helper', function(done) { + var entry = extend({}, logEntry); + delete entry.metadata.severity; + + log.notice(entry, done); + }); + + it('should write to a log with warning helper', function(done) { + var entry = extend({}, logEntry); + delete entry.metadata.severity; + + log.warning(entry, done); + }); + + it('should write from a stream', function(done) { + log.createWriteStream() + .on('error', done) + .on('finish', done) + .end(logEntry); + }); + }); +}); + +function generateUniqueName() { + return 'gcloud-test-logging-' + uuid.v1(); +} diff --git a/test/logging/index.js b/test/logging/index.js new file mode 100644 index 000000000000..b7761f0a56b0 --- /dev/null +++ b/test/logging/index.js @@ -0,0 +1,716 @@ +/** + * Copyright 2015 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 arrify = require('arrify'); +var assert = require('assert'); +var extend = require('extend'); +var mockery = require('mockery'); +var nodeutil = require('util'); + +var Service = require('../../lib/common/service.js'); +var util = require('../../lib/common/util.js'); + +var extended = false; +var fakeStreamRouter = { + extend: function(Class, methods) { + if (Class.name !== 'Logging') { + return; + } + + extended = true; + methods = arrify(methods); + assert.deepEqual(methods, [ + 'getLogs', + 'getServices', + 'getSinks' + ]); + } +}; + +var fakeUtil = extend({}, util, { + makeAuthenticatedRequestFactory: util.noop +}); + +function FakeBucket() { + this.calledWith_ = arguments; +} + +function FakeDataset() { + this.calledWith_ = arguments; +} + +function FakeTopic() { + this.calledWith_ = arguments; +} + +function FakeLog() { + this.calledWith_ = arguments; +} + +function FakeServiceClass() { + this.calledWith_ = arguments; +} + +function FakeSink() { + this.calledWith_ = arguments; +} + +function FakeService() { + this.calledWith_ = arguments; + Service.apply(this, arguments); +} + +nodeutil.inherits(FakeService, Service); + +describe('Logging', function() { + var Logging; + var logging; + + var PROJECT_ID = 'project-id'; + + before(function() { + mockery.registerMock('../common/service.js', FakeService); + mockery.registerMock('../common/stream-router.js', fakeStreamRouter); + mockery.registerMock('../common/util.js', fakeUtil); + mockery.registerMock('../bigquery/dataset.js', FakeDataset); + mockery.registerMock('../pubsub/topic.js', FakeTopic); + mockery.registerMock('../storage/bucket.js', FakeBucket); + mockery.registerMock('./log.js', FakeLog); + mockery.registerMock('./service.js', FakeServiceClass); + mockery.registerMock('./sink.js', FakeSink); + + mockery.enable({ + useCleanCache: true, + warnOnUnregistered: false + }); + + Logging = require('../../lib/logging/index.js'); + }); + + after(function() { + mockery.deregisterAll(); + mockery.disable(); + }); + + beforeEach(function() { + logging = new Logging({ + projectId: PROJECT_ID + }); + + logging.request = util.noop; + }); + + after(function() { + mockery.deregisterAll(); + mockery.disable(); + }); + + describe('instantiation', function() { + it('should extend the correct methods', function() { + assert(extended); // See `fakeStreamRouter.extend` + }); + + it('should normalize the arguments', function() { + var options = { + projectId: PROJECT_ID, + credentials: 'credentials', + email: 'email', + keyFilename: 'keyFile' + }; + + var normalizeArguments = fakeUtil.normalizeArguments; + var normalizeArgumentsCalled = false; + var fakeContext = {}; + + fakeUtil.normalizeArguments = function(context, options_) { + normalizeArgumentsCalled = true; + assert.strictEqual(context, fakeContext); + assert.strictEqual(options, options_); + return options_; + }; + + Logging.call(fakeContext, options); + assert(normalizeArgumentsCalled); + + fakeUtil.normalizeArguments = normalizeArguments; + }); + + it('should inherit from Service', function() { + assert(logging instanceof Service); + + var calledWith = logging.calledWith_[0]; + + var baseUrl = 'https://logging.googleapis.com/v1beta3'; + assert.strictEqual(calledWith.baseUrl, baseUrl); + assert.deepEqual(calledWith.scopes, [ + 'https://www.googleapis.com/auth/logging.admin' + ]); + }); + }); + + describe('createSink', function() { + var SINK_NAME = 'name'; + + it('should throw if a name is not provided', function() { + assert.throws(function() { + logging.createSink(); + }, 'A sink name must be provided.'); + }); + + it('should throw if a config object is not provided', function() { + assert.throws(function() { + logging.createSink(SINK_NAME); + }, 'A sink configuration object must be provided.'); + }); + + describe('bucket destination', function() { + var bucket = new FakeBucket(); + bucket.name = 'bucket-name'; + bucket.acl = { + owners: { + addGroup: util.noop + } + }; + + var CONFIG = { + destination: bucket + }; + + it('should add cloud-logs as an owner', function(done) { + bucket.acl.owners.addGroup = function(entity) { + assert.strictEqual(entity, 'cloud-logs@google.com'); + done(); + }; + + logging.createSink(SINK_NAME, CONFIG, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = {}; + + beforeEach(function() { + bucket.acl.owners.addGroup = function(entity, callback) { + callback(error, apiResponse); + }; + }); + + it('should return error and API response to callback', function(done) { + logging.createSink(SINK_NAME, CONFIG, function(err, sink, resp) { + assert.strictEqual(err, error); + assert.strictEqual(sink, null); + assert.strictEqual(resp, apiResponse); + + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = {}; + + beforeEach(function() { + bucket.acl.owners.addGroup = function(entity, callback) { + callback(null, apiResponse); + }; + }); + + it('should call createSink with string destination', function(done) { + bucket.acl.owners.addGroup = function(entity, callback) { + logging.createSink = function(name, config, callback) { + assert.strictEqual(name, SINK_NAME); + + assert.strictEqual(config, CONFIG); + + var expectedDestination = 'storage.googleapis.com/' + bucket.name; + assert.strictEqual(config.destination, expectedDestination); + + callback(); // done() + }; + + callback(null, apiResponse); + }; + + logging.createSink(SINK_NAME, CONFIG, done); + }); + }); + }); + + describe('dataset destination', function() { + var dataset = new FakeDataset(); + dataset.id = 'dataset-id'; + dataset.parent = { + projectId: PROJECT_ID + }; + + var CONFIG = { + destination: dataset + }; + + it('should set the correct destination', function(done) { + logging.request = function(reqOpts) { + var expectedDestination = [ + 'bigquery.googleapis.com', + 'projects', + dataset.parent.projectId, + 'datasets', + dataset.id + ].join('/'); + assert.strictEqual(reqOpts.json.destination, expectedDestination); + + done(); + }; + + logging.createSink(SINK_NAME, CONFIG, assert.ifError); + }); + }); + + describe('topic destination', function() { + var topic = new FakeTopic(); + topic.name = 'topic-name'; + + var CONFIG = { + destination: topic + }; + + it('should set the correct destination', function(done) { + logging.request = function(reqOpts) { + var expectedDestination = 'pubsub.googleapis.com/' + topic.name; + assert.strictEqual(reqOpts.json.destination, expectedDestination); + + done(); + }; + + logging.createSink(SINK_NAME, CONFIG, assert.ifError); + }); + }); + + describe('API request', function() { + it('should make the correct API request', function(done) { + var config = { + a: 'b', + c: 'd' + }; + + var expectedConfig = extend({}, config, { + name: SINK_NAME + }); + + logging.request = function(reqOpts) { + assert.strictEqual(reqOpts.method, 'POST'); + assert.strictEqual(reqOpts.uri, '/sinks'); + assert.deepEqual(reqOpts.json, expectedConfig); + + done(); + }; + + logging.createSink(SINK_NAME, config, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = {}; + + beforeEach(function() { + logging.request = function(reqOpts, callback) { + callback(error, apiResponse); + }; + }); + + it('should exec callback with error & API response', function(done) { + logging.createSink(SINK_NAME, {}, function(err, sink, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(sink, null); + assert.strictEqual(apiResponse_, apiResponse); + + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + name: SINK_NAME + }; + + beforeEach(function() { + logging.request = function(reqOpts, callback) { + callback(null, apiResponse); + }; + }); + + it('should exec callback with Sink & API response', function(done) { + var sink = {}; + + logging.sink = function(name_) { + assert.strictEqual(name_, SINK_NAME); + return sink; + }; + + logging.createSink(SINK_NAME, {}, function(err, sink_, apiResponse_) { + assert.ifError(err); + + assert.strictEqual(sink_, sink); + assert.strictEqual(sink_.metadata, apiResponse); + assert.strictEqual(apiResponse_, apiResponse); + + done(); + }); + }); + }); + }); + }); + + describe('getLogs', function() { + it('should accept only a callback', function(done) { + logging.request = function(reqOpts) { + assert.deepEqual(reqOpts.qs, {}); + done(); + }; + + logging.getLogs(assert.ifError); + }); + + it('should make the correct API request', function(done) { + var options = {}; + + logging.request = function(reqOpts) { + assert.strictEqual(reqOpts.uri, '/logs'); + assert.strictEqual(reqOpts.qs, options); + done(); + }; + + logging.getLogs(options, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = {}; + + beforeEach(function() { + logging.request = function(reqOpts, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + logging.getLogs({}, function(err, logs, nextQuery, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(logs, null); + assert.strictEqual(nextQuery, null); + assert.strictEqual(apiResponse_, apiResponse); + + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + logs: [ + { + name: 'log-name' + } + ] + }; + + beforeEach(function() { + logging.request = function(reqOpts, callback) { + callback(null, apiResponse); + }; + }); + + it('should build a nextQuery if necessary', function(done) { + var nextPageToken = 'next-page-token'; + var apiResponseWithNextPageToken = extend({}, apiResponse, { + nextPageToken: nextPageToken + }); + var expectedNextQuery = { + pageToken: nextPageToken + }; + + logging.request = function(reqOpts, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + logging.getLogs({}, function(err, logs, nextQuery) { + assert.ifError(err); + + assert.deepEqual(nextQuery, expectedNextQuery); + + done(); + }); + }); + + it('should execute callback with Logs & API resp', function(done) { + var log = {}; + + logging.log = function(name) { + assert.strictEqual(name, apiResponse.logs[0].name); + return log; + }; + + logging.getLogs({}, function(err, logs, nextQuery, apiResponse_) { + assert.ifError(err); + + assert.strictEqual(logs[0], log); + assert.strictEqual(logs[0].metadata, apiResponse.logs[0]); + + assert.strictEqual(apiResponse_, apiResponse); + + done(); + }); + }); + }); + }); + + describe('getServices', function() { + it('should accept only a callback', function(done) { + logging.request = function(reqOpts) { + assert.deepEqual(reqOpts.qs, {}); + done(); + }; + + logging.getServices(assert.ifError); + }); + + it('should make the correct API request', function(done) { + var options = {}; + + logging.request = function(reqOpts) { + assert.strictEqual(reqOpts.uri, '/logServices'); + assert.strictEqual(reqOpts.qs, options); + done(); + }; + + logging.getServices(options, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = {}; + + beforeEach(function() { + logging.request = function(reqOpts, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + logging.getServices({}, function(err, services, nextQuery, resp) { + assert.strictEqual(err, error); + assert.strictEqual(services, null); + assert.strictEqual(nextQuery, null); + assert.strictEqual(resp, apiResponse); + + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + logServices: [ + { + name: 'service-name' + } + ] + }; + + beforeEach(function() { + logging.request = function(reqOpts, callback) { + callback(null, apiResponse); + }; + }); + + it('should build a nextQuery if necessary', function(done) { + var nextPageToken = 'next-page-token'; + var apiResponseWithNextPageToken = extend({}, apiResponse, { + nextPageToken: nextPageToken + }); + var expectedNextQuery = { + pageToken: nextPageToken + }; + + logging.request = function(reqOpts, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + logging.getServices({}, function(err, services, nextQuery) { + assert.ifError(err); + + assert.deepEqual(nextQuery, expectedNextQuery); + + done(); + }); + }); + + it('should execute callback with Logs & API resp', function(done) { + var service = {}; + + logging.service = function(name) { + assert.strictEqual(name, apiResponse.logServices[0].name); + return service; + }; + + logging.getServices({}, function(err, services, nextQuery, resp) { + assert.ifError(err); + + assert.strictEqual(services[0], service); + assert.strictEqual(services[0].metadata, apiResponse.logServices[0]); + + assert.strictEqual(resp, apiResponse); + + done(); + }); + }); + }); + }); + + describe('getSinks', function() { + it('should accept only a callback', function(done) { + logging.request = function() { + done(); + }; + + logging.getSinks(assert.ifError); + }); + + it('should make the correct API request', function(done) { + logging.request = function(reqOpts) { + assert.strictEqual(reqOpts.uri, '/sinks'); + done(); + }; + + logging.getSinks({}, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = {}; + + beforeEach(function() { + logging.request = function(reqOpts, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + logging.getSinks({}, function(err, sinks, nextQuery, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(sinks, null); + assert.strictEqual(nextQuery, null); + assert.strictEqual(apiResponse_, apiResponse); + + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + sinks: [ + { + name: 'sink-name' + } + ] + }; + + beforeEach(function() { + logging.request = function(reqOpts, callback) { + callback(null, apiResponse); + }; + }); + + it('should build a nextQuery if necessary', function(done) { + var nextPageToken = 'next-page-token'; + var apiResponseWithNextPageToken = extend({}, apiResponse, { + nextPageToken: nextPageToken + }); + var expectedNextQuery = { + pageToken: nextPageToken + }; + + logging.request = function(reqOpts, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + logging.getSinks({}, function(err, sinks, nextQuery) { + assert.ifError(err); + + assert.deepEqual(nextQuery, expectedNextQuery); + + done(); + }); + }); + + it('should execute callback with Logs & API resp', function(done) { + var log = {}; + + logging.sink = function(name) { + assert.strictEqual(name, apiResponse.sinks[0].name); + return log; + }; + + logging.getSinks({}, function(err, sinks, nextQuery, apiResponse_) { + assert.ifError(err); + + assert.strictEqual(sinks[0], log); + assert.strictEqual(sinks[0].metadata, apiResponse.sinks[0]); + + assert.strictEqual(apiResponse_, apiResponse); + + done(); + }); + }); + }); + }); + + describe('log', function() { + var NAME = 'log-name'; + + it('should return a Log object', function() { + var log = logging.log(NAME); + assert(log instanceof FakeLog); + assert.strictEqual(log.calledWith_[0], logging); + assert.strictEqual(log.calledWith_[1], NAME); + }); + }); + + describe('service', function() { + var NAME = 'service-name'; + + it('should return a Log object', function() { + var service = logging.service(NAME); + assert(service instanceof FakeServiceClass); + assert.strictEqual(service.calledWith_[0], logging); + assert.strictEqual(service.calledWith_[1], NAME); + }); + }); + + describe('sink', function() { + var NAME = 'sink-name'; + + it('should return a Log object', function() { + var sink = logging.sink(NAME); + assert(sink instanceof FakeSink); + assert.strictEqual(sink.calledWith_[0], logging); + assert.strictEqual(sink.calledWith_[1], NAME); + }); + }); +}); diff --git a/test/logging/log.js b/test/logging/log.js new file mode 100644 index 000000000000..551e55e117ec --- /dev/null +++ b/test/logging/log.js @@ -0,0 +1,534 @@ +/** + * Copyright 2015 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 concat = require('concat-stream'); +var extend = require('extend'); +var mockery = require('mockery'); +var nodeutil = require('util'); +var through = require('through2'); + +var ServiceObject = require('../../lib/common/service-object.js'); +var util = require('../../lib/common/util.js'); + +function FakeServiceObject() { + this.calledWith_ = arguments; + ServiceObject.apply(this, arguments); +} + +nodeutil.inherits(FakeServiceObject, ServiceObject); + + +describe('Log', function() { + var Log; + var log; + + var LOGGING = {}; + var LOG_NAME_UNFORMATTED = 'custom/path/logs/log-name'; + var LOG_NAME = 'log-name'; + + var assignSeverityToEntriesOverride = null; + + before(function() { + mockery.registerMock('../common/service-object.js', FakeServiceObject); + + mockery.enable({ + useCleanCache: true, + warnOnUnregistered: false + }); + + Log = require('../../lib/logging/log.js'); + var assignSeverityToEntries_ = Log.assignSeverityToEntries_; + Log.assignSeverityToEntries_ = function() { + return (assignSeverityToEntriesOverride || assignSeverityToEntries_) + .apply(null, arguments); + }; + }); + + after(function() { + mockery.deregisterAll(); + mockery.disable(); + }); + + beforeEach(function() { + assignSeverityToEntriesOverride = null; + log = new Log(LOGGING, LOG_NAME_UNFORMATTED); + }); + + describe('instantiation', function() { + it('should inherit from ServiceObject', function() { + assert(log instanceof ServiceObject); + + var calledWith = log.calledWith_[0]; + + assert.strictEqual(calledWith.parent, LOGGING); + assert.strictEqual(calledWith.baseUrl, '/logs'); + assert.strictEqual(calledWith.id, LOG_NAME); + assert.deepEqual(calledWith.methods, { + delete: true + }); + }); + + it('should localize the name', function() { + assert.strictEqual(log.name, LOG_NAME); + }); + + it('should localize the original name', function() { + assert.strictEqual(log.unformattedName, LOG_NAME_UNFORMATTED); + }); + }); + + describe('assignSeverityToEntries_', function() { + var ENTRIES = [ + {}, + {} + ]; + + var SEVERITY = 'severity'; + + it('should assign severity property to every entry', function() { + var entries = Log.assignSeverityToEntries_(ENTRIES, SEVERITY); + + var allEntriesAssignedWithSeverity = entries.every(function(entry) { + return entry.metadata.severity === SEVERITY; + }); + + assert.strictEqual(allEntriesAssignedWithSeverity, true); + }); + + it('should not affect original array', function() { + var originalEntries = extend({}, ENTRIES); + + Log.assignSeverityToEntries_(originalEntries, SEVERITY); + + assert.deepEqual(originalEntries, ENTRIES); + }); + }); + + describe('createWriteStream', function() { + beforeEach(function() { + log.request = util.noop; + }); + + it('should return a writable object stream', function() { + var ws = log.createWriteStream(); + + assert.strictEqual(ws.writable, true); + }); + + it('should make a request once writing started', function(done) { + log.request = function(reqOpts) { + assert.strictEqual(reqOpts.method, 'POST'); + assert.strictEqual(reqOpts.uri, '/entries:write'); + + setImmediate(done); + + return through(); + }; + + var ws = log.createWriteStream(); + ws.emit('writing'); + }); + + it('should emit the response from the request', function(done) { + var response = {}; + var ws = log.createWriteStream(); + + log.request = function() { + var stream = through(); + + setImmediate(function() { + stream.emit('response', response); + }); + + return stream; + }; + + ws.on('response', function(response_) { + assert.strictEqual(response_, response); + done(); + }); + + ws.emit('writing'); + }); + + it('should format each entry', function(done) { + var entry = { formatted: false }; + var formattedEntry = { formatted: true }; + + var ws = log.createWriteStream(); + + var expectedData = { + entries: [ + formattedEntry, + formattedEntry + ] + }; + + var requestStream = concat(function(data) { + assert.deepEqual(JSON.parse(data), expectedData); + done(); + }); + + log.request = function() { + return requestStream; + }; + + log.formatEntry_ = function(entry_) { + assert.strictEqual(entry_, entry); + return formattedEntry; + }; + + ws.write(entry); + ws.end(entry); + }); + }); + + describe('insert', function() { + var ENTRY = {}; + var LABELS = []; + + it('should not require labels', function(done) { + log.request = function(reqOpts) { + assert.deepEqual(reqOpts.json.commonLabels, []); + done(); + }; + + log.insert(ENTRY, assert.ifError); + }); + + it('should make the correct API request', function(done) { + var formattedEntry = {}; + + log.formatEntry_ = function() { + return formattedEntry; + }; + + log.request = function(reqOpts) { + assert.strictEqual(reqOpts.method, 'POST'); + assert.strictEqual(reqOpts.uri, '/entries:write'); + assert.strictEqual(reqOpts.json.commonLabels, LABELS); + assert.strictEqual(reqOpts.json.entries[0], formattedEntry); + + done(); + }; + + log.insert(ENTRY, LABELS, assert.ifError); + }); + + it('should exec callback with only error and API response', function(done) { + var args = [1, 2, 3, 4]; + + log.request = function(reqOpts, callback) { + callback.apply(null, args); + }; + + log.insert(ENTRY, LABELS, function() { + assert.strictEqual(arguments.length, 2); + + assert.strictEqual(arguments[0], args[0]); + assert.strictEqual(arguments[1], args[1]); + + done(); + }); + }); + }); + + describe('severity helpers', function() { + var ENTRY = {}; + var LABELS = []; + + beforeEach(function() { + log.insert = util.noop; + }); + + describe('alert', function() { + it('should format the entries', function(done) { + assignSeverityToEntriesOverride = function(entries, severity) { + assert.strictEqual(entries, ENTRY); + assert.strictEqual(severity, 'ALERT'); + + done(); + }; + + log.alert(ENTRY, LABELS, assert.ifError); + }); + + it('should pass correct arguments to insert', function(done) { + var assignedEntries = []; + + assignSeverityToEntriesOverride = function() { + return assignedEntries; + }; + + log.insert = function(entry, labels, callback) { + assert.strictEqual(entry, assignedEntries); + assert.strictEqual(labels, LABELS); + callback(); // done() + }; + + log.alert(ENTRY, LABELS, done); + }); + }); + + describe('critical', function() { + it('should format the entries', function(done) { + assignSeverityToEntriesOverride = function(entries, severity) { + assert.strictEqual(entries, ENTRY); + assert.strictEqual(severity, 'CRITICAL'); + + done(); + }; + + log.critical(ENTRY, LABELS, assert.ifError); + }); + + it('should pass correct arguments to insert', function(done) { + var assignedEntries = []; + + assignSeverityToEntriesOverride = function() { + return assignedEntries; + }; + + log.insert = function(entry, labels, callback) { + assert.strictEqual(entry, assignedEntries); + assert.strictEqual(labels, LABELS); + callback(); // done() + }; + + log.critical(ENTRY, LABELS, done); + }); + }); + + describe('debug', function() { + it('should format the entries', function(done) { + assignSeverityToEntriesOverride = function(entries, severity) { + assert.strictEqual(entries, ENTRY); + assert.strictEqual(severity, 'DEBUG'); + + done(); + }; + + log.debug(ENTRY, LABELS, assert.ifError); + }); + + it('should pass correct arguments to insert', function(done) { + var assignedEntries = []; + + assignSeverityToEntriesOverride = function() { + return assignedEntries; + }; + + log.insert = function(entry, labels, callback) { + assert.strictEqual(entry, assignedEntries); + assert.strictEqual(labels, LABELS); + callback(); // done() + }; + + log.debug(ENTRY, LABELS, done); + }); + }); + + describe('emergency', function() { + it('should format the entries', function(done) { + assignSeverityToEntriesOverride = function(entries, severity) { + assert.strictEqual(entries, ENTRY); + assert.strictEqual(severity, 'EMERGENCY'); + + done(); + }; + + log.emergency(ENTRY, LABELS, assert.ifError); + }); + + it('should pass correct arguments to insert', function(done) { + var assignedEntries = []; + + assignSeverityToEntriesOverride = function() { + return assignedEntries; + }; + + log.insert = function(entry, labels, callback) { + assert.strictEqual(entry, assignedEntries); + assert.strictEqual(labels, LABELS); + callback(); // done() + }; + + log.emergency(ENTRY, LABELS, done); + }); + }); + + describe('error', function() { + it('should format the entries', function(done) { + assignSeverityToEntriesOverride = function(entries, severity) { + assert.strictEqual(entries, ENTRY); + assert.strictEqual(severity, 'ERROR'); + + done(); + }; + + log.error(ENTRY, LABELS, assert.ifError); + }); + + it('should pass correct arguments to insert', function(done) { + var assignedEntries = []; + + assignSeverityToEntriesOverride = function() { + return assignedEntries; + }; + + log.insert = function(entry, labels, callback) { + assert.strictEqual(entry, assignedEntries); + assert.strictEqual(labels, LABELS); + callback(); // done() + }; + + log.error(ENTRY, LABELS, done); + }); + }); + + describe('info', function() { + it('should format the entries', function(done) { + assignSeverityToEntriesOverride = function(entries, severity) { + assert.strictEqual(entries, ENTRY); + assert.strictEqual(severity, 'INFO'); + + done(); + }; + + log.info(ENTRY, LABELS, assert.ifError); + }); + + it('should pass correct arguments to insert', function(done) { + var assignedEntries = []; + + assignSeverityToEntriesOverride = function() { + return assignedEntries; + }; + + log.insert = function(entry, labels, callback) { + assert.strictEqual(entry, assignedEntries); + assert.strictEqual(labels, LABELS); + callback(); // done() + }; + + log.info(ENTRY, LABELS, done); + }); + }); + + describe('notice', function() { + it('should format the entries', function(done) { + assignSeverityToEntriesOverride = function(entries, severity) { + assert.strictEqual(entries, ENTRY); + assert.strictEqual(severity, 'NOTICE'); + + done(); + }; + + log.notice(ENTRY, LABELS, assert.ifError); + }); + + it('should pass correct arguments to insert', function(done) { + var assignedEntries = []; + + assignSeverityToEntriesOverride = function() { + return assignedEntries; + }; + + log.insert = function(entry, labels, callback) { + assert.strictEqual(entry, assignedEntries); + assert.strictEqual(labels, LABELS); + callback(); // done() + }; + + log.notice(ENTRY, LABELS, done); + }); + }); + + describe('warning', function() { + it('should format the entries', function(done) { + assignSeverityToEntriesOverride = function(entries, severity) { + assert.strictEqual(entries, ENTRY); + assert.strictEqual(severity, 'WARNING'); + + done(); + }; + + log.warning(ENTRY, LABELS, assert.ifError); + }); + + it('should pass correct arguments to insert', function(done) { + var assignedEntries = []; + + assignSeverityToEntriesOverride = function() { + return assignedEntries; + }; + + log.insert = function(entry, labels, callback) { + assert.strictEqual(entry, assignedEntries); + assert.strictEqual(labels, LABELS); + callback(); // done() + }; + + log.warning(ENTRY, LABELS, done); + }); + }); + }); + + describe('formatEntry_', function() { + var ENTRY_OBJECT = { + data: {} + }; + + var ENTRY_STRING = { + data: 'entry' + }; + + it('should not modify the original object', function() { + var originalEntryObject = extend({}, ENTRY_OBJECT); + + log.formatEntry_(ENTRY_OBJECT); + + assert.deepEqual(originalEntryObject, ENTRY_OBJECT); + }); + + it('should assign the log name', function() { + var entry = log.formatEntry_(ENTRY_OBJECT); + + assert.strictEqual(entry.log, log.name); + }); + + it('should assign a structPayload property if an object', function() { + var entry = log.formatEntry_(ENTRY_OBJECT); + + assert.deepEqual(entry.structPayload, ENTRY_OBJECT.data); + }); + + it('should assign a textPayload if a string', function() { + var entry = log.formatEntry_(ENTRY_STRING); + + assert.strictEqual(entry.textPayload, ENTRY_STRING.data); + }); + + it('should delete the data property', function() { + var entry = log.formatEntry_(ENTRY_OBJECT); + + assert.strictEqual(entry.data, undefined); + }); + }); +}); diff --git a/test/logging/service.js b/test/logging/service.js new file mode 100644 index 000000000000..164d0e0130b1 --- /dev/null +++ b/test/logging/service.js @@ -0,0 +1,188 @@ +/** + * Copyright 2015 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 arrify = require('arrify'); +var assert = require('assert'); +var extend = require('extend'); +var mockery = require('mockery'); +var nodeutil = require('util'); + +var ServiceObject = require('../../lib/common/service-object.js'); + +var extended = false; +var fakeStreamRouter = { + extend: function(Class, methods) { + if (Class.name !== 'Service') { + return; + } + + extended = true; + methods = arrify(methods); + assert.deepEqual(methods, [ + 'getIndexes' + ]); + } +}; + +function FakeServiceObject() { + this.calledWith_ = arguments; + ServiceObject.apply(this, arguments); +} + +nodeutil.inherits(FakeServiceObject, ServiceObject); + +describe('Service', function() { + var Service; + var service; + + var LOGGING = {}; + var SERVICE_NAME = 'service-name'; + + before(function() { + mockery.registerMock('../common/service-object.js', FakeServiceObject); + mockery.registerMock('../common/stream-router.js', fakeStreamRouter); + + mockery.enable({ + useCleanCache: true, + warnOnUnregistered: false + }); + + Service = require('../../lib/logging/service.js'); + }); + + after(function() { + mockery.deregisterAll(); + mockery.disable(); + }); + + beforeEach(function() { + service = new Service(LOGGING, SERVICE_NAME); + }); + + describe('instantiation', function() { + it('should extend the correct methods', function() { + assert(extended); // See `fakeStreamRouter.extend` + }); + + it('should inherit from ServiceObject', function() { + assert(service instanceof ServiceObject); + + var calledWith = service.calledWith_[0]; + + assert.strictEqual(calledWith.parent, LOGGING); + assert.strictEqual(calledWith.baseUrl, '/logServices'); + assert.strictEqual(calledWith.id, SERVICE_NAME); + assert.deepEqual(calledWith.methods, {}); + }); + + it('should localize the name', function() { + assert.strictEqual(service.name, SERVICE_NAME); + }); + }); + + describe('getIndexes', function() { + it('should accept only a callback', function(done) { + service.request = function(reqOpts) { + assert.deepEqual(reqOpts.qs, {}); + done(); + }; + + service.getIndexes(assert.ifError); + }); + + it('should make the correct API request', function(done) { + var options = {}; + + service.request = function(reqOpts) { + assert.strictEqual(reqOpts.uri, '/indexes'); + assert.strictEqual(reqOpts.qs, options); + done(); + }; + + service.getIndexes(options, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = {}; + + beforeEach(function() { + service.request = function(reqOpts, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + service.getIndexes({}, function(err, indexes, nextQuery, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(indexes, null); + assert.strictEqual(nextQuery, null); + assert.strictEqual(apiResponse_, apiResponse); + + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + serviceIndexPrefixes: 'index-' + }; + + beforeEach(function() { + service.request = function(reqOpts, callback) { + callback(null, apiResponse); + }; + }); + + it('should build a nextQuery if necessary', function(done) { + var nextPageToken = 'next-page-token'; + var apiResponseWithNextPageToken = extend({}, apiResponse, { + nextPageToken: nextPageToken + }); + var expectedNextQuery = { + pageToken: nextPageToken + }; + + service.request = function(reqOpts, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + service.getIndexes({}, function(err, indexes, nextQuery) { + assert.ifError(err); + + assert.deepEqual(nextQuery, expectedNextQuery); + + done(); + }); + }); + + it('should execute callback with indexes & API resp', function(done) { + service.getIndexes({}, function(err, indexes, nextQuery, apiResponse_) { + assert.ifError(err); + + assert.strictEqual(indexes[0], apiResponse.serviceIndexPrefixes); + + assert.strictEqual(apiResponse_, apiResponse); + + done(); + }); + }); + }); + }); +}); diff --git a/test/logging/sink.js b/test/logging/sink.js new file mode 100644 index 000000000000..dfdf748a82b8 --- /dev/null +++ b/test/logging/sink.js @@ -0,0 +1,200 @@ +/** + * Copyright 2015 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 mockery = require('mockery'); +var nodeutil = require('util'); + +var ServiceObject = require('../../lib/common/service-object.js'); +var util = require('../../lib/common/util.js'); + +function FakeServiceObject() { + this.calledWith_ = arguments; + ServiceObject.apply(this, arguments); +} + +nodeutil.inherits(FakeServiceObject, ServiceObject); + +describe('Sink', function() { + var Sink; + var sink; + + var LOGGING = { + createSink: util.noop + }; + var SINK_NAME = 'sink-name'; + + before(function() { + mockery.registerMock('../common/service-object.js', FakeServiceObject); + + mockery.enable({ + useCleanCache: true, + warnOnUnregistered: false + }); + + Sink = require('../../lib/logging/sink.js'); + }); + + after(function() { + mockery.deregisterAll(); + mockery.disable(); + }); + + beforeEach(function() { + sink = new Sink(LOGGING, SINK_NAME); + }); + + describe('instantiation', function() { + it('should inherit from ServiceObject', function(done) { + var loggingInstance = extend({}, LOGGING, { + createSink: { + bind: function(context) { + assert.strictEqual(context, loggingInstance); + done(); + } + } + }); + + var sink = new Sink(loggingInstance, SINK_NAME); + assert(sink instanceof ServiceObject); + + var calledWith = sink.calledWith_[0]; + + assert.strictEqual(calledWith.parent, loggingInstance); + assert.strictEqual(calledWith.baseUrl, '/sinks'); + assert.strictEqual(calledWith.id, SINK_NAME); + assert.deepEqual(calledWith.methods, { + create: true, + delete: true, + getMetadata: true + }); + }); + + it('should localize the name', function() { + assert.strictEqual(sink.name, SINK_NAME); + }); + }); + + describe('setFilter', function() { + var FILTER = 'filter'; + + it('should call set metadata', function(done) { + sink.setMetadata = function(metadata, callback) { + assert.strictEqual(metadata.filter, FILTER); + callback(); // done() + }; + + sink.setFilter(FILTER, done); + }); + }); + + describe('setMetadata', function() { + var METADATA = { a: 'b', c: 'd' }; + + it('should refresh the metadata', function(done) { + sink.getMetadata = function() { + done(); + }; + + sink.setMetadata(METADATA, assert.ifError); + }); + + it('should exec callback with error from refresh', function(done) { + var error = new Error('Error.'); + var apiResponse = {}; + + sink.getMetadata = function(callback) { + callback(error, null, apiResponse); + }; + + sink.setMetadata(METADATA, function(err, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should make the correct request', function(done) { + var currentMetadata = { e: 'f', g: 'h' }; + + sink.getMetadata = function(callback) { + callback(null, currentMetadata); + }; + + sink.request = function(reqOpts) { + assert.strictEqual(reqOpts.uri, ''); + assert.strictEqual(reqOpts.method, 'PUT'); + + var expectedMetadata = extend({}, METADATA, currentMetadata); + assert.deepEqual(reqOpts.json, expectedMetadata); + + done(); + }; + + sink.setMetadata(METADATA, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = {}; + + beforeEach(function() { + sink.getMetadata = function(callback) { + callback(); + }; + + sink.request = function(reqOpts, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + sink.setMetadata(METADATA, function(err, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(apiResponse_, apiResponse); + + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = {}; + + beforeEach(function() { + sink.getMetadata = function(callback) { + callback(); + }; + + sink.request = function(reqOpts, callback) { + callback(null, apiResponse); + }; + }); + + it('should execute callback with API resp', function(done) { + sink.setMetadata(METADATA, function(err, apiResponse_) { + assert.ifError(err); + assert.strictEqual(apiResponse_, apiResponse); + + done(); + }); + }); + }); + }); +});