UNPKG

7.62 kBJavaScriptView Raw
1/**
2 * Copyright 2015 Google Inc. All Rights Reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17'use strict';
18
19var utils = require('@google/cloud-diagnostics-common').utils;
20var traceLabels = require('./trace-labels.js');
21var pjson = require('../package.json');
22var constants = require('./constants.js');
23
24/* @const {Array<string>} list of scopes needed to operate with the trace API */
25var SCOPES = ['https://www.googleapis.com/auth/trace.append'];
26
27var headers = {};
28headers[constants.TRACE_AGENT_REQUEST_HEADER] = 1;
29
30/**
31 * Creates a basic trace writer.
32 * @param {!Logger} logger
33 * @constructor
34 */
35function TraceWriter(logger, config) {
36 /** @private */
37 this.logger_ = logger;
38
39 /** @private */
40 this.config_ = config;
41
42 /** @private {function} authenticated request function */
43 this.request_ = utils.authorizedRequestFactory(SCOPES);
44
45 /** @private {Array<string>} stringified traces to be published */
46 this.buffer_ = [];
47
48 /** @private {Object} default labels to be attached to written spans */
49 this.defaultLabels_ = {};
50
51 /** @private {Boolean} whether the trace writer is active */
52 this.isActive = true;
53
54 // Schedule periodic flushing of the buffer, but only if we are able to get
55 // the project number (potentially from the network.)
56 var that = this;
57 that.getProjectNumber(function(err, project) {
58 if (err) { return; } // ignore as index.js takes care of this.
59 that.scheduleFlush_(project);
60 });
61
62 that.getHostname(function(hostname) {
63 that.getInstanceId(function(instanceId) {
64 var labels = {};
65 labels[traceLabels.AGENT_DATA] = 'node ' + pjson.version;
66 labels[traceLabels.GCE_HOSTNAME] = hostname;
67 if (instanceId) {
68 labels[traceLabels.GCE_INSTANCE_ID] = instanceId;
69 }
70 var moduleName = process.env.GAE_MODULE_NAME || hostname;
71 labels[traceLabels.GAE_MODULE_NAME] = moduleName;
72
73 var moduleVersion = process.env.GAE_MODULE_VERSION;
74 if (moduleVersion) {
75 labels[traceLabels.GAE_MODULE_VERSION] = moduleVersion;
76 var minorVersion = process.env.GAE_MINOR_VERSION;
77 if (minorVersion) {
78 var versionLabel = '';
79 if (moduleName !== 'default') {
80 versionLabel = moduleName + ':';
81 }
82 versionLabel += moduleVersion + '.' + minorVersion;
83 labels[traceLabels.GAE_VERSION] = versionLabel;
84 }
85 }
86 Object.freeze(labels);
87 that.defaultLabels_ = labels;
88 });
89 });
90}
91
92TraceWriter.prototype.stop = function() {
93 this.isActive = false;
94};
95
96TraceWriter.prototype.getHostname = function(cb) {
97 var that = this;
98 utils.getHostname(headers, function(err, hostname) {
99 if (err && err.code !== 'ENOTFOUND') {
100 // We are running on GCP.
101 that.logger_.warn('Unable to retrieve GCE hostname.', err);
102 }
103 cb(hostname || require('os').hostname());
104 });
105};
106
107TraceWriter.prototype.getInstanceId = function(cb) {
108 var that = this;
109 utils.getInstanceId(headers, function(err, instanceId) {
110 if (err && err.code !== 'ENOTFOUND') {
111 // We are running on GCP.
112 that.logger_.warn('Unable to retrieve GCE instance id.', err);
113 }
114 cb(instanceId);
115 });
116};
117
118/**
119 * Ensures that all sub spans of the provided spanData are
120 * closed and then queues the span data to be published.
121 *
122 * @param {SpanData} spanData The trace to be queued.
123 */
124TraceWriter.prototype.writeSpan = function(spanData) {
125 for (var i = 0; i < spanData.trace.spans.length; i++) {
126 if (spanData.trace.spans[i].endTime === '') {
127 spanData.trace.spans[i].close();
128 }
129 }
130
131 // Copy properties from the default labels.
132 for (var k in this.defaultLabels_) {
133 if (this.defaultLabels_.hasOwnProperty(k)) {
134 spanData.addLabel(k, this.defaultLabels_[k]);
135 }
136 }
137 this.queueTrace_(spanData.trace);
138};
139
140/**
141 * Buffers the provided trace to be published.
142 *
143 * @private
144 * @param {Trace} trace The trace to be queued.
145 */
146TraceWriter.prototype.queueTrace_ = function(trace) {
147 var that = this;
148
149 that.getProjectNumber(function(err, project) {
150 if (err) {
151 that.logger_.info('No project number, dropping trace.');
152 return; // ignore as index.js takes care of this.
153 }
154
155 trace.projectId = project;
156 that.buffer_.push(JSON.stringify(trace));
157 that.logger_.debug('queued trace. new size:', that.buffer_.length);
158
159 // Publish soon if the buffer is getting big
160 if (that.buffer_.length >= that.config_.bufferSize) {
161 that.logger_.info('Flushing: trace buffer full');
162 setImmediate(function() { that.flushBuffer_(project); });
163 }
164 });
165};
166
167/**
168 * Flushes the buffer of traces at a regular interval
169 * controlled by the flushDelay property of this
170 * TraceWriter's config.
171 */
172TraceWriter.prototype.scheduleFlush_ = function(project) {
173 this.logger_.info('Flushing: performing periodic flush');
174 this.flushBuffer_(project);
175
176 // Do it again after delay
177 if (this.isActive) {
178 setTimeout(this.scheduleFlush_.bind(this, project),
179 this.config_.flushDelaySeconds * 1000).unref();
180 }
181};
182
183/**
184 * Serializes the buffered traces to be published asynchronously.
185 *
186 * @param {number} projectId The id of the project that traces should publish on.
187 */
188TraceWriter.prototype.flushBuffer_ = function(projectId) {
189 if (this.buffer_.length === 0) {
190 return;
191 }
192
193 // Privatize and clear the buffer.
194 var buffer = this.buffer_;
195 this.buffer_ = [];
196 this.logger_.debug('Flushing traces', buffer);
197 this.publish_(projectId, '{"traces":[' + buffer.join() + ']}');
198};
199
200/**
201 * Publishes flushed traces to the network.
202 *
203 * @param {number} projectId The id of the project that traces should publish on.
204 * @param {string} json The stringified json representation of the queued traces.
205 */
206TraceWriter.prototype.publish_ = function(projectId, json) {
207 var that = this;
208 var uri = 'https://cloudtrace.googleapis.com/v1/projects/' +
209 projectId + '/traces';
210
211 this.request_({
212 method: 'PATCH',
213 uri: uri,
214 body: json,
215 headers: headers
216 }, function(err, response, body) {
217 if (err) {
218 that.logger_.error('TraceWriter: error: ',
219 (response && response.statusCode) || '', err);
220 } else {
221 that.logger_.info('TraceWriter: published. statusCode: ' + response.statusCode);
222 }
223 });
224};
225
226/**
227 * Returns the project number if it has been cached and attempts to load
228 * it from the enviroment or network otherwise.
229 *
230 * @param {function(?, number):?} callback an (err, result) style callback
231 */
232TraceWriter.prototype.getProjectNumber = function(callback) {
233 var that = this;
234 if (that.config_.projectId) {
235 callback(null, that.config_.projectId);
236 return;
237 }
238
239 utils.getProjectNumber(headers, function(err, project) {
240 if (err) {
241 callback(err);
242 return;
243 }
244 that.logger_.info('Acquired ProjectId from metadata: ' + project);
245 that.config_.projectId = project;
246 callback(null, project);
247 });
248};
249
250/**
251 * Export TraceWriter.
252 * FIXME(ofrobots): TraceWriter should be a singleton. We should export
253 * a get function that returns the instance instead.
254 */
255module.exports = TraceWriter;