UNPKG

9.8 kBJavaScriptView Raw
1const superagent = require('superagent');
2const formatDate = require('./formatDate');
3
4const DEFAULT_INTERVAL = 0;
5const DEFAULT_BATCH = 0;
6const NOOP = () => {};
7
8function getUUID() {
9 // eslint gets funny about bitwise
10 /* eslint-disable */
11 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
12 const piece = (Math.random() * 16) | 0;
13 const elem = c === 'x' ? piece : (piece & 0x3) | 0x8;
14 return elem.toString(16);
15 });
16 /* eslint-enable */
17}
18
19/**
20 * Axios has been replaced with SuperAgent (issue #28), to maintain some backwards
21 * compatibility the SuperAgent response object is marshaled to conform
22 * to the Axios response object
23 */
24function marshalHttpResponse(response) {
25 return {
26 data: response.body,
27 status: response.status,
28 statusText: response.statusText || response.res.statusMessage,
29 headers: response.headers,
30 request: response.xhr || response.req,
31 config: response.req
32 };
33}
34
35class SumoLogger {
36 constructor(options) {
37 if (
38 !options ||
39 !Object.prototype.hasOwnProperty.call(options, 'endpoint') ||
40 options.endpoint === undefined ||
41 options.endpoint === ''
42 ) {
43 console.error('An endpoint value must be provided');
44 return;
45 }
46
47 this.config = {};
48 this.pendingLogs = [];
49 this.interval = 0;
50
51 this.setConfig(options);
52 this.startLogSending();
53 }
54
55 setConfig(newConfig) {
56 this.config = {
57 endpoint: newConfig.endpoint,
58 returnPromise: Object.prototype.hasOwnProperty.call(
59 newConfig,
60 'returnPromise'
61 )
62 ? newConfig.returnPromise
63 : true,
64 clientUrl: newConfig.clientUrl || '',
65 useIntervalOnly: newConfig.useIntervalOnly || false,
66 interval: newConfig.interval || DEFAULT_INTERVAL,
67 batchSize: newConfig.batchSize || DEFAULT_BATCH,
68 sourceName: newConfig.sourceName || '',
69 hostName: newConfig.hostName || '',
70 sourceCategory: newConfig.sourceCategory || '',
71 session: newConfig.sessionKey || getUUID(),
72 onSuccess: newConfig.onSuccess || NOOP,
73 onError: newConfig.onError || NOOP,
74 graphite: newConfig.graphite || false,
75 raw: newConfig.raw || false
76 };
77 }
78
79 updateConfig(newConfig = {}) {
80 if (newConfig.endpoint) {
81 this.config.endpoint = newConfig.endpoint;
82 }
83 if (newConfig.returnPromise) {
84 this.config.returnPromise = newConfig.returnPromise;
85 }
86 if (newConfig.useIntervalOnly) {
87 this.config.useIntervalOnly = newConfig.useIntervalOnly;
88 }
89 if (newConfig.interval) {
90 this.config.interval = newConfig.interval;
91 this.startLogSending();
92 }
93 if (newConfig.batchSize) {
94 this.config.batchSize = newConfig.batchSize;
95 }
96 if (newConfig.sourceCategory) {
97 this.config.sourceCategory = newConfig.sourceCategory;
98 }
99 }
100
101 batchReadyToSend() {
102 if (this.config.batchSize === 0) {
103 return this.config.interval === 0;
104 } else {
105 const pendingMessages = this.pendingLogs.reduce((acc, curr) => {
106 const log = JSON.parse(curr);
107 return acc + log.msg + '\n';
108 }, '');
109 const pendingBatchSize = pendingMessages.length;
110 const ready = pendingBatchSize >= this.config.batchSize;
111 if (ready) {
112 this.stopLogSending();
113 }
114 return ready;
115 }
116 }
117
118 _postSuccess(logsSentLength) {
119 this.pendingLogs = this.pendingLogs.slice(logsSentLength);
120 // Reset interval if needed:
121 this.startLogSending();
122 this.config.onSuccess();
123 }
124
125 sendLogs() {
126 if (this.pendingLogs.length === 0) {
127 return false;
128 }
129
130 try {
131 const headers = {
132 'X-Sumo-Client': 'sumo-javascript-sdk'
133 };
134 if (this.config.graphite) {
135 Object.assign(headers, {
136 'Content-Type': 'application/vnd.sumologic.graphite'
137 });
138 } else {
139 Object.assign(headers, { 'Content-Type': 'application/json' });
140 }
141 if (this.config.sourceName !== '') {
142 Object.assign(headers, {
143 'X-Sumo-Name': this.config.sourceName
144 });
145 }
146 if (this.config.sourceCategory !== '') {
147 Object.assign(headers, {
148 'X-Sumo-Category': this.config.sourceCategory
149 });
150 }
151 if (this.config.hostName !== '') {
152 Object.assign(headers, { 'X-Sumo-Host': this.config.hostName });
153 }
154
155 if (this.config.returnPromise && this.pendingLogs.length === 1) {
156 return superagent
157 .post(this.config.endpoint)
158 .set(headers)
159 .send(this.pendingLogs.join('\n'))
160 .then(marshalHttpResponse)
161 .then(res => {
162 this._postSuccess(1);
163 return res;
164 })
165 .catch(error => {
166 this.config.onError(error);
167 return Promise.reject(error);
168 });
169 }
170
171 const logsToSend = Array.from(this.pendingLogs);
172 return superagent
173 .post(this.config.endpoint)
174 .set(headers)
175 .send(logsToSend.join('\n'))
176 .then(marshalHttpResponse)
177 .then(() => this._postSuccess(logsToSend.length))
178 .catch(error => {
179 this.config.onError(error);
180 if (this.config.returnPromise) {
181 return Promise.reject(error);
182 }
183 });
184 } catch (ex) {
185 this.config.onError(ex);
186 return false;
187 }
188 }
189
190 startLogSending() {
191 if (this.config.interval > 0) {
192 if (this.interval) {
193 this.stopLogSending();
194 }
195 this.interval = setInterval(() => {
196 this.sendLogs();
197 }, this.config.interval);
198 }
199 }
200
201 stopLogSending() {
202 clearInterval(this.interval);
203 }
204
205 emptyLogQueue() {
206 this.pendingLogs = [];
207 }
208
209 flushLogs() {
210 return this.sendLogs();
211 }
212
213 log(msg, optionalConfig) {
214 let message = msg;
215
216 if (!message) {
217 console.error('A value must be provided');
218 return false;
219 }
220
221 const isArray = message instanceof Array;
222 const testEl = isArray ? message[0] : message;
223 const type = typeof testEl;
224
225 if (type === 'undefined') {
226 console.error('A value must be provided');
227 return false;
228 }
229
230 if (
231 this.config.graphite &&
232 (!Object.prototype.hasOwnProperty.call(testEl, 'path') ||
233 !Object.prototype.hasOwnProperty.call(testEl, 'value'))
234 ) {
235 console.error(
236 'Both "path" and "value" properties must be provided in the message object to send Graphite metrics'
237 );
238 return false;
239 }
240
241 if (type === 'object') {
242 if (Object.keys(message).length === 0) {
243 console.error('A non-empty JSON object must be provided');
244 return false;
245 }
246 }
247
248 if (!isArray) {
249 message = [message];
250 }
251
252 let ts = new Date();
253 let sessKey = this.config.session;
254 const client = { url: this.config.clientUrl };
255
256 if (optionalConfig) {
257 if (
258 Object.prototype.hasOwnProperty.call(
259 optionalConfig,
260 'sessionKey'
261 )
262 ) {
263 sessKey = optionalConfig.sessionKey;
264 }
265
266 if (
267 Object.prototype.hasOwnProperty.call(
268 optionalConfig,
269 'timestamp'
270 )
271 ) {
272 ts = optionalConfig.timestamp;
273 }
274
275 if (Object.prototype.hasOwnProperty.call(optionalConfig, 'url')) {
276 client.url = optionalConfig.url;
277 }
278 }
279
280 const timestamp = formatDate(ts);
281
282 const messages = message.map(item => {
283 if (this.config.graphite) {
284 return `${item.path} ${item.value} ${Math.round(
285 ts.getTime() / 1000
286 )}`;
287 }
288 if (this.config.raw) {
289 return item;
290 }
291 if (typeof item === 'string') {
292 return JSON.stringify(
293 Object.assign(
294 {
295 msg: item,
296 sessionId: sessKey,
297 timestamp
298 },
299 client
300 )
301 );
302 }
303 const current = {
304 sessionId: sessKey,
305 timestamp
306 };
307 return JSON.stringify(Object.assign(current, client, item));
308 });
309
310 this.pendingLogs = this.pendingLogs.concat(messages);
311
312 if (!this.config.useIntervalOnly && this.batchReadyToSend()) {
313 return this.sendLogs();
314 }
315 }
316}
317
318module.exports = SumoLogger;
319
\No newline at end of file