From b9fadbc807295fe8d44ac5793491ccf2fd6f99b6 Mon Sep 17 00:00:00 2001 From: Stephen Sawchuk Date: Mon, 11 Jul 2016 13:57:56 -0400 Subject: [PATCH] logging: support native types (#1374) * logging: support native types * add unit tests * add docs --- lib/common/grpc-service.js | 106 ++++++++++--- lib/logging/entry.js | 21 ++- system-test/logging.js | 62 +++++++- test/common/grpc-service.js | 288 ++++++++++++++++++++++++------------ test/logging/entry.js | 9 +- 5 files changed, 369 insertions(+), 117 deletions(-) diff --git a/lib/common/grpc-service.js b/lib/common/grpc-service.js index d69af7e0b7b..4d69778a90a 100644 --- a/lib/common/grpc-service.js +++ b/lib/common/grpc-service.js @@ -291,21 +291,53 @@ GrpcService.prototype.request = function(protoOpts, reqOpts, callback) { }); }; +/** + * Decode a protobuf Struct's value. + * + * @private + * + * @param {object} value - A Struct's Field message. + * @return {*} - The decoded value. + */ +GrpcService.decodeValue_ = function(value) { + switch (value.kind) { + case 'structValue': { + return GrpcService.structToObj_(value.structValue); + } + + case 'nullValue': { + return null; + } + + case 'listValue': { + return value.listValue.values.map(GrpcService.decodeValue_); + } + + default: { + return value[value.kind]; + } + } +}; + /** * Convert a raw value to a type-denoted protobuf message-friendly object. * * @private * * @param {*} value - The input value. - * @return {*} - The converted value. + * @param {object=} options - Configuration object. + * @param {boolean} options.stringify - Stringify un-recognized types. + * @return {*} - The encoded value. * * @example - * GrpcService.convertValue_('Hi'); + * GrpcService.encodeValue_('Hi'); * // { * // stringValue: 'Hello!' * // } */ -GrpcService.convertValue_ = function(value) { +GrpcService.encodeValue_ = function(value, options) { + options = options || {}; + var convertedValue; if (is.null(value)) { @@ -332,24 +364,20 @@ GrpcService.convertValue_ = function(value) { convertedValue = { structValue: GrpcService.objToStruct_(value) }; - } else if (is.date(value)) { - var seconds = value.getTime() / 1000; - var secondsRounded = Math.floor(seconds); - - convertedValue = { - timestampValue: { - seconds: secondsRounded, - nanos: Math.floor((seconds - secondsRounded) * 1e9) - } - }; } else if (is.array(value)) { convertedValue = { listValue: { - values: value.map(GrpcService.convertValue_) + values: value.map(GrpcService.encodeValue_) } }; } else { - throw new Error('Value of type ' + typeof value + ' not recognized.'); + if (!options.stringify) { + throw new Error('Value of type ' + typeof value + ' not recognized.'); + } + + convertedValue = { + stringValue: String(value) + }; } return convertedValue; @@ -361,6 +389,8 @@ GrpcService.convertValue_ = function(value) { * @private * * @param {object} obj - An object to convert. + * @param {object=} options - Configuration object. + * @param {boolean} options.stringify - Stringify un-recognized types. * @return {array} - The converted object. * * @example @@ -403,14 +433,56 @@ GrpcService.convertValue_ = function(value) { * // } * // } */ -GrpcService.objToStruct_ = function(obj) { +GrpcService.objToStruct_ = function(obj, options) { + options = options || {}; + var convertedObject = { fields: {} }; for (var prop in obj) { if (obj.hasOwnProperty(prop)) { - convertedObject.fields[prop] = GrpcService.convertValue_(obj[prop]); + var value = obj[prop]; + + if (is.undefined(value)) { + continue; + } + + convertedObject.fields[prop] = GrpcService.encodeValue_(value, options); + } + } + + return convertedObject; +}; + +/** + * Condense a protobuf Struct into an object of only its values. + * + * @private + * + * @param {object} struct - A protobuf Struct message. + * @return {object} - The simplified object. + * + * @example + * GrpcService.structToObj_({ + * fields: { + * name: { + * kind: 'stringValue', + * stringValue: 'Stephen' + * } + * } + * }); + * // { + * // name: 'Stephen' + * // } + */ +GrpcService.structToObj_ = function(struct) { + var convertedObject = {}; + + for (var prop in struct.fields) { + if (struct.fields.hasOwnProperty(prop)) { + var value = struct.fields[prop]; + convertedObject[prop] = GrpcService.decodeValue_(value); } } diff --git a/lib/logging/entry.js b/lib/logging/entry.js index 6e7e1b1ca78..d567613f993 100644 --- a/lib/logging/entry.js +++ b/lib/logging/entry.js @@ -41,6 +41,16 @@ var GrpcService = require('../common/grpc-service.js'); * [Monitored Resource](https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/MonitoredResource). * @param {object|string} data - The data to use as the value for this log * entry. + * + * If providing an object, these value types are supported: + * - `String` + * - `Number` + * - `Boolean` + * - `Buffer` + * - `Object` + * - `Array` + * + * Any other types are stringified with `String(value)`. * @return {module:logging/entry} * * @example @@ -98,10 +108,15 @@ function Entry(resource, data) { * * @param {object} entry - An API representation of an entry. See a * [LogEntry](https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/LogEntry). - * @return {module:logging/entity} + * @return {module:logging/entry} */ Entry.fromApiResponse_ = function(entry) { var data = entry[entry.payload]; + + if (entry.payload === 'jsonPayload') { + data = GrpcService.structToObj_(data); + } + var serializedEntry = extend(new Entry(entry.resource, data), entry); if (serializedEntry.timestamp) { @@ -145,7 +160,9 @@ Entry.prototype.toJSON = function() { } if (is.object(this.data)) { - entry.jsonPayload = GrpcService.objToStruct_(this.data); + entry.jsonPayload = GrpcService.objToStruct_(this.data, { + serialize: true + }); } else if (is.string(this.data)) { entry.textPayload = this.data; } diff --git a/system-test/logging.js b/system-test/logging.js index 64ae3aea3d3..6644201c8d6 100644 --- a/system-test/logging.js +++ b/system-test/logging.js @@ -21,6 +21,7 @@ var async = require('async'); var exec = require('methmeth'); var format = require('string-format-obj'); var is = require('is'); +var prop = require('propprop'); var uuid = require('node-uuid'); var env = require('./env.js'); @@ -31,6 +32,7 @@ var Storage = require('../lib/storage/index.js'); describe('Logging', function() { var TESTS_PREFIX = 'gcloud-logging-test'; + var WRITE_CONSISTENCY_DELAY_MS = 15000; var logging = new Logging(env); @@ -313,11 +315,65 @@ describe('Logging', function() { }); it('should write multiple entries to a log', function(done) { - log.write(logEntries, options, done); + log.write(logEntries, options, function(err) { + assert.ifError(err); + + setTimeout(function() { + log.getEntries({ + pageSize: logEntries.length + }, function(err, entries) { + assert.ifError(err); + + assert.deepEqual(entries.map(prop('data')).reverse(), [ + 'log entry 1', + { + delegate: 'my_username' + }, + { + nonValue: null, + boolValue: true, + arrayValue: [ 1, 2, 3 ] + }, + { + nested: { + delegate: 'my_username' + } + } + ]); + + done(); + }); + }, WRITE_CONSISTENCY_DELAY_MS); + }); }); - it('should write a single entry with alert helper', function(done) { - log.alert(logEntries[0], options, done); + it('should write an entry with primitive values', function(done) { + var logEntry = log.entry({ + when: new Date(), + matchUser: /username: (.+)/, + matchUserError: new Error('No user found.'), + shouldNotBeSaved: undefined + }); + + log.write(logEntry, options, function(err) { + assert.ifError(err); + + setTimeout(function() { + log.getEntries({ pageSize: 1 }, function(err, entries) { + assert.ifError(err); + + var entry = entries[0]; + + assert.deepEqual(entry.data, { + when: logEntry.data.when.toString(), + matchUser: logEntry.data.matchUser.toString(), + matchUserError: logEntry.data.matchUserError.toString() + }); + + done(); + }); + }, WRITE_CONSISTENCY_DELAY_MS); + }); }); it('should write to a log with alert helper', function(done) { diff --git a/test/common/grpc-service.js b/test/common/grpc-service.js index 7f28f8b9d9c..5f2ae1c85b5 100644 --- a/test/common/grpc-service.js +++ b/test/common/grpc-service.js @@ -322,6 +322,200 @@ describe('GrpcService', function() { }); }); + describe('decodeValue_', function() { + it('should decode a struct value', function() { + var structValue = { + kind: 'structValue', + structValue: {} + }; + + var decodedValue = {}; + + GrpcService.structToObj_ = function() { + return decodedValue; + }; + + assert.strictEqual(GrpcService.decodeValue_(structValue), decodedValue); + }); + + it('should decode a null value', function() { + var nullValue = { + kind: 'nullValue' + }; + + var decodedValue = null; + + assert.strictEqual(GrpcService.decodeValue_(nullValue), decodedValue); + }); + + it('should decode a list value', function() { + var listValue = { + kind: 'listValue', + listValue: { + values: [ + { + kind: 'nullValue' + } + ] + } + }; + + assert.deepEqual(GrpcService.decodeValue_(listValue), [null]); + }); + + it('should return the raw value', function() { + var numberValue = { + kind: 'numberValue', + numberValue: 8 + }; + + assert.strictEqual(GrpcService.decodeValue_(numberValue), 8); + }); + }); + + describe('encodeValue_', function() { + it('should convert primitive values correctly', function() { + var buffer = new Buffer('Value'); + + assert.deepEqual(GrpcService.encodeValue_(null), { + nullValue: 0 + }); + + assert.deepEqual(GrpcService.encodeValue_(1), { + numberValue: 1 + }); + + assert.deepEqual(GrpcService.encodeValue_('Hi'), { + stringValue: 'Hi' + }); + + assert.deepEqual(GrpcService.encodeValue_(true), { + boolValue: true + }); + + assert.strictEqual( + GrpcService.encodeValue_(buffer).blobValue.toString(), + 'Value' + ); + }); + + it('should convert objects', function() { + var value = {}; + + GrpcService.objToStruct_ = function() { + return value; + }; + + var convertedValue = GrpcService.encodeValue_(value); + + assert.deepEqual(convertedValue, { + structValue: value + }); + }); + + it('should convert arrays', function() { + var convertedValue = GrpcService.encodeValue_([1, 2, 3]); + + assert.deepEqual(convertedValue.listValue, { + values: [ + GrpcService.encodeValue_(1), + GrpcService.encodeValue_(2), + GrpcService.encodeValue_(3) + ] + }); + }); + + it('should throw if a type is not recognized', function() { + assert.throws(function() { + GrpcService.encodeValue_(); + }, 'Value of type undefined not recognized.'); + }); + + describe('options.stringify', function() { + var OPTIONS = { + stringify: true + }; + + it('should return a string if the value is not recognized', function() { + var date = new Date(); + + assert.deepEqual( + GrpcService.encodeValue_(date, OPTIONS), + { stringValue: String(date) } + ); + }); + }); + }); + + describe('objToStruct_', function() { + it('should convert values in an Object', function() { + var inputValue = {}; + var convertedValue = {}; + + GrpcService.encodeValue_ = function(value) { + assert.strictEqual(value, inputValue); + return convertedValue; + }; + + var struct = GrpcService.objToStruct_({ + a: inputValue + }); + + assert.strictEqual(struct.fields.a, convertedValue); + }); + + it('should not include undefined values', function() { + var inputValue = {}; + var convertedValue = {}; + + GrpcService.encodeValue_ = function(value) { + assert.strictEqual(value, inputValue); + return convertedValue; + }; + + var struct = GrpcService.objToStruct_({ + a: undefined, + b: inputValue + }); + + assert.strictEqual(struct.fields.a, undefined); + assert.strictEqual(struct.fields.b, convertedValue); + }); + + it('should pass options to encodeValue', function(done) { + var options = {}; + + GrpcService.encodeValue_ = function(value, options_) { + assert.strictEqual(options_, options); + done(); + }; + + GrpcService.objToStruct_({ a: {} }, options); + }); + }); + + describe('structToObj_', function() { + it('should convert a struct to an object', function() { + var inputValue = {}; + var decodedValue = {}; + + var struct = { + fields: { + a: inputValue + } + }; + + GrpcService.decodeValue_ = function(value) { + assert.strictEqual(value, inputValue); + return decodedValue; + }; + + assert.deepEqual(GrpcService.structToObj_(struct), { + a: decodedValue + }); + }); + }); + describe('request', function() { var PROTO_OPTS = { service: 'service', method: 'method', timeout: 3000 }; var REQ_OPTS = {}; @@ -680,100 +874,6 @@ describe('GrpcService', function() { }); }); - describe('convertValue_', function() { - it('should convert primitive values correctly', function() { - var buffer = new Buffer('Value'); - - assert.deepEqual(GrpcService.convertValue_(null), { - nullValue: 0 - }); - - assert.deepEqual(GrpcService.convertValue_(1), { - numberValue: 1 - }); - - assert.deepEqual(GrpcService.convertValue_('Hi'), { - stringValue: 'Hi' - }); - - assert.deepEqual(GrpcService.convertValue_(true), { - boolValue: true - }); - - assert.strictEqual( - GrpcService.convertValue_(buffer).blobValue.toString(), - 'Value' - ); - }); - - it('should convert objects', function() { - var value = {}; - - GrpcService.objToStruct_ = function() { - return value; - }; - - var convertedValue = GrpcService.convertValue_(value); - - assert.deepEqual(convertedValue, { - structValue: value - }); - }); - - it('should convert dates', function() { - var value = new Date(); - var seconds = value.getTime() / 1000; - var secondsRounded = Math.floor(seconds); - - var convertedValue = GrpcService.convertValue_(value); - - assert.deepEqual(convertedValue, { - timestampValue: { - seconds: secondsRounded, - nanos: Math.floor((seconds - secondsRounded) * 1e9) - } - }); - }); - - it('should convert arrays', function() { - var convertedValue = GrpcService.convertValue_([1, 2, 3]); - - assert.deepEqual(convertedValue.listValue, { - values: [ - GrpcService.convertValue_(1), - GrpcService.convertValue_(2), - GrpcService.convertValue_(3) - ] - }); - }); - - it('should throw if a type is not recognized', function() { - assert.throws(function() { - GrpcService.convertValue_(); - }, 'Value of type undefined not recognized.'); - }); - }); - - describe('objToStruct_', function() { - it('should convert values in an Object', function() { - var inputValue = {}; - var convertedValue = {}; - - GrpcService.convertValue_ = function(value) { - assert.strictEqual(value, inputValue); - return convertedValue; - }; - - var obj = { - a: inputValue - }; - - var struct = GrpcService.objToStruct_(obj); - - assert.strictEqual(struct.fields.a, convertedValue); - }); - }); - describe('getGrpcCredentials_', function() { it('should get credentials from the auth client', function(done) { grpcService.authClient = { diff --git a/test/logging/entry.js b/test/logging/entry.js index e6842ef2a77..0916992beee 100644 --- a/test/logging/entry.js +++ b/test/logging/entry.js @@ -73,6 +73,10 @@ describe('Entry', function() { var seconds = date.getTime() / 1000; var secondsRounded = Math.floor(seconds); + FakeGrpcService.structToObj_ = function(data) { + return data; + }; + entry = Entry.fromApiResponse_({ resource: RESOURCE, payload: 'jsonPayload', @@ -178,8 +182,11 @@ describe('Entry', function() { var input = {}; var converted = {}; - FakeGrpcService.objToStruct_ = function(obj) { + FakeGrpcService.objToStruct_ = function(obj, options) { assert.strictEqual(obj, input); + assert.deepEqual(options, { + serialize: true + }); return converted; };