forked from peterherrmann/BetterLog
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Code.gs
430 lines (387 loc) · 15.3 KB
/
Code.gs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
/*************************************************************************
* Globals
*********/
var sheet_; //the spreadsheet that is appended to
var SHEET_MAX_ROWS = 50000; //sheet is cleared and starts again
var SHEET_LOG_CELL_WIDTH = 1000; //
var SHEET_LOG_HEADER = 'Message layout: Date Time UTC-Offset MillisecondsSinceInvoked LogLevel Message. Use Ctrl↓ (or Command↓) to jump to the last row';
var DATE_TIME_LAYOUT = 'yyyy-MM-dd HH:mm:ss:SSS Z'; //http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html
var JSON_SPACES = 0; //the number of space characters to use as white space;
//ref http://docs.oracle.com/javase/7/docs/api/java/util/logging/Level.html
var Level = Object.freeze({
OFF: Number.MAX_VALUE,
SEVERE: 1000,
WARNING:900,
INFO: 800,
CONFIG: 700,
FINE: 500,
FINER: 400,
FINEST: 300,
ALL: Number.MIN_VALUE});
var level_ = Level.INFO; //set as default. The log level. We log everything this level or greater.
var START_TIME = new Date(); //so we can calculate elapsed time;
var thisApp_ = this;
var counter = 0;
var nativeLogger_ = Logger;
/*************************************************************************
* public methods
*********/
/**
* Allows logging to a Google spreadsheet.
*
* @param {String} optKey The spreadsheet key (optional). Defaults to the active spreadsheet if available.
* @param {String} optSheetName The name of the sheet (optional). Defaults to "Log". The sheet is created if needed.
* @returns {BetterLog} this object, for chaining
*/
function useSpreadsheet(optKey, optSheetName) {
if (hasAuth_()) {
setLogSheet_(optKey, optSheetName);
sheet_.getRange(1,1).setValue(SHEET_LOG_HEADER); //in case we need to update
rollLogOver_(); //rollover the log if we need to
}
return thisApp_;
}
/**
* Logs at the SEVERE level. SEVERE is a message level indicating a serious failure.
* In general SEVERE messages should describe events that are of considerable importance and
* which will prevent normal program execution. They should be reasonably intelligible to end users and to system administrators.
*
* @param {Object} message The message to log or an sprintf-like format string (uses Utilities.formatString() internally - see http://www.perlmonks.org/?node_id=20519 as a good reference).
* @param {Object...} optValues If a format string is used in the message, a number of values to insert into the format string.
* @returns {BetterLog} this object, for chaining
*/
function severe(message, optValues) {
var lev = Level.SEVERE;
if (isLoggable_(lev)) {
log_(arguments, lev);
}
return thisApp_;
}
/**
* Logs at the WARNING level. WARNING is a message level indicating a potential problem.
* In general WARNING messages should describe events that will be of interest to end users
* or system managers, or which indicate potential problems.
*
* @param {Object} message The message to log or an sprintf-like format string (uses Utilities.formatString() internally - see http://www.perlmonks.org/?node_id=20519 as a good reference).
* @param {Object...} optValues If a format string is used in the message, a number of values to insert into the format string.
* @returns {BetterLog} this object, for chaining
*/
function warning(message, optValues) {
var lev = Level.WARNING;
if (isLoggable_(lev)) {
log_(arguments, lev);
}
return thisApp_;
}
/**
* Logs at the INFO level. INFO is a message level for informational messages.
* Typically INFO messages will be written to the console or its equivalent. So the INFO level
* should only be used for reasonably significant messages that will make sense to end users and system administrators.
<h3>Examples:</h3>
<pre>
function myFunction() {
//Best practice for using BetterLog and logging to a spreadsheet:
// You can add and set the property "BetterLogLevel" in File > Project Properties and change it to
// "OFF","SEVERE","WARNING","INFO","CONFIG","FINE","FINER","FINEST" or "ALL" at runtime without editing code.
Logger = BetterLog.setLevel(ScriptProperties.getProperty('BetterLogLevel')) //defaults to 'INFO' level
.useSpreadsheet('0AhDqyd_bUCmvdDdGczRlX00zUlBMeGNLeE9SNlJ0VGc'); //automatically rolls over at 50,000 rows
Logger.log('Messages using Logger.log continue to work');
Logger.config('The current log level is %s', Logger.getLevel());
Logger.finer('Entering the "%s" function', arguments.callee.name); //only logged if level is FINER, FINEST or ALL.
Logger.info('Starting my function that does stuff');
//Do our work
for (var i = 0; i < 5; i++) {
//do detailed stuff
Logger.finest('Inside the for loop that does the xyz work. i is currently: %d', i);
}
Logger.info('My work is complete and I performed %d iterations', i);
Logger.finer('Returning from the "%s" function', arguments.callee.name);
}
</pre>
*
* @param {Object} message The message to log or an sprintf-like format string (uses Utilities.formatString() internally - see http://www.perlmonks.org/?node_id=20519 as a good reference).
* @param {Object...} optValues If a format string is used in the message, a number of values to insert into the format string.
* @returns {BetterLog} this object, for chaining
*/
function info(message, optValues) {
var lev = Level.INFO;
if (isLoggable_(lev)) {
log_(arguments, lev);
}
return thisApp_;
}
/**
* Logs at the CONFIG level. CONFIG is a message level for static configuration messages.
* CONFIG messages are intended to provide a variety of static configuration information,
* to assist in debugging problems that may be associated with particular configurations.
*
* @param {Object} message The message to log or an sprintf-like format string (uses Utilities.formatString() internally - see http://www.perlmonks.org/?node_id=20519 as a good reference).
* @param {Object...} optValues If a format string is used in the message, a number of values to insert into the format string.
* @returns {BetterLog} this object, for chaining
*/
function config(message, optValues) {
var lev = Level.CONFIG;
if (isLoggable_(lev)) {
log_(arguments, lev);
}
return thisApp_;
}
/**
* Logs at the FINE level. FINE is a message level providing tracing information.
* All of FINE, FINER, and FINEST are intended for relatively detailed tracing.
* The exact meaning of the three levels will vary between subsystems, but in general,
* FINEST should be used for the most voluminous detailed output,
* FINER for somewhat less detailed output, and FINE for the lowest volume (and most important) messages.
*
* In general the FINE level should be used for information that will be broadly interesting to developers
* who do not have a specialized interest in the specific subsystem.
* FINE messages might include things like minor (recoverable) failures. Issues indicating potential performance problems are also worth logging as FINE. T
*
* @param {Object} message The message to log or an sprintf-like format string (uses Utilities.formatString() internally - see http://www.perlmonks.org/?node_id=20519 as a good reference).
* @param {Object...} optValues If a format string is used in the message, a number of values to insert into the format string.
* @returns {BetterLog} this object, for chaining
*/
function fine(message, optValues) {
var lev = Level.FINE;
if (isLoggable_(lev)) {
log_(arguments, lev);
}
return thisApp_;
}
/**
* Logs at the FINER level. FINER indicates a fairly detailed tracing message.
* By default logging calls for entering, returning, or throwing an exception are traced at this level.
*
* @param {Object} message The message to log or an sprintf-like format string (uses Utilities.formatString() internally - see http://www.perlmonks.org/?node_id=20519 as a good reference).
* @param {Object...} optValues If a format string is used in the message, a number of values to insert into the format string.
* @returns {BetterLog} this object, for chaining
*/
function finer(message, optValues) {
var lev = Level.FINER;
if (isLoggable_(lev)) {
log_(arguments, lev);
}
return thisApp_;
}
/**
* Logs at the FINEST level. FINEST indicates a highly detailed tracing message.
*
* @param {Object} message The message to log or an sprintf-like format string (uses Utilities.formatString() internally - see http://www.perlmonks.org/?node_id=20519 as a good reference).
* @param {Object...} optValues If a format string is used in the message, a number of values to insert into the format string.
* @returns {BetterLog} this object, for chaining
*/
function finest(message, optValues) {
var lev = Level.FINEST;
if (isLoggable_(lev)) {
log_(arguments, lev);
}
return thisApp_;
}
/**
* Logs at the INFO level. INFO is a message level for informational messages.
* Typically INFO messages will be written to the console or its equivalent. So the INFO level should
* only be used for reasonably significant messages that will make sense to end users and system administrators.
*
* @param {Object} message The message to log or an sprintf-like format string (uses Utilities.formatString() internally - see http://www.perlmonks.org/?node_id=20519 as a good reference).
* @param {Object...} optValues If a format string is used in the message, a number of values to insert into the format string.
* @returns {BetterLog} this object, for chaining
*/
function log(message, optValues) {
return info.apply(this, arguments);
}
/**
* Sets the new log level
*
* @param {String} logLevel The new log level e.g. "OFF","SEVERE","WARNING","INFO","CONFIG","FINE","FINER","FINEST" or "ALL".
* @returns {BetterLog} this object, for chaining
*/
function setLevel(logLevel) {
if (typeof logLevel === "string") {
var logLevel = stringToLevel_(logLevel);
}
if (logLevel != getLevel_()) {
setLevel_(logLevel);
}
return thisApp_;
}
/**
* Gets the current log level name
*
* @returns {String} The name of the current log level e.g. "OFF","SEVERE","WARNING","INFO","CONFIG","FINE","FINER","FINEST" or "ALL".
*/
function getLevel() {
return levelToString_(getLevel_());
}
/*************************************************************************
* @private functions
********************/
//in custom function, you do not have permission to call getActiveUser
function hasAuth_() {
try {
Session.getActiveUser();
return true;
} catch (e) {
return false;
}
}
// Returns the string as a Level.
function stringToLevel_(str) {
for (var name in Level) {
if (name == str) {
return Level[name];
}
}
}
// Returns the Level as a String
function levelToString_(lvl) {
for (var name in Level) {
if (Level[name] == lvl)
return name;
}
}
//gets the current logging level
function getLevel_() {
return level_;
}
//sets the current logging level
function setLevel_(lvl) {
for (var name in Level) {
if (Level[name] == lvl) {
level_ = lvl;
info("Log level has been set to " + getLevel());
break;
}
}
}
//checks to see if this level is enabled
function isLoggable_(Level) {
if (getLevel_()<=Level) {
return true;
}
return false;
}
//core logger function
function log_(msgArgs, level) {
counter++;
// get args and transform objects to strings like the native logger does.
var args = Array.prototype.slice.call(msgArgs).map(function(e){
var type = typeof e;
if (type === 'undefined') return 'undefined';
return e !== null && type === 'object' ? JSON.stringify(e, null, JSON_SPACES) : e;
});
var msg = (typeof msgArgs[0] == 'string' || msgArgs[0] instanceof String) ? Utilities.formatString.apply(this, args) : msgArgs[0];
//default console logging (built in with Google Apps Script's View > Logs...)
nativeLogger_.log(convertUsingDefaultPatternLayout_(msg, level));
//stackdriver console logging
if (typeof console !== "undefined" && typeof console.time !== "undefined") {
switch(level) {
case Level.INFO:
console.info(msg);
break;
case Level.SEVERE:
console.error(msg);
break;
case Level.WARNING:
console.warn(msg);
break;
default:
console.info(msg);
}
}
//ss logging
if (sheet_) {
logToSheet_(msg, level);
}
}
// rolls over the log if we need to
function rollLogOver_() {
var rowCount = call_(function() {return sheet_.getLastRow();});
if (rowCount > SHEET_MAX_ROWS) {
// get a lock or throw exception
var lock = LockService.getScriptLock();
lock.waitLock(10000); //try for 10 secs to get a lock, long enough to rollover the log
//copy the log
var ss = sheet_.getParent();
var oldLog = ss.copy(ss.getName() + ' as at ' + Utilities.formatDate(new Date(), Session.getScriptTimeZone(), DATE_TIME_LAYOUT));
//add current viewers and editors to old log
oldLog.addViewers(ss.getViewers());
oldLog.addEditors(ss.getEditors());
// prep the live log
sheet_.deleteRows(2, sheet_.getMaxRows()-2);
sheet_.getRange(1,1).setValue(SHEET_LOG_HEADER);
//update the log
sheet_.getRange("A2").setValue(['Log reached ' + rowCount + ' rows (MAX_ROWS is ' + SHEET_MAX_ROWS + ') and was cleared. Previous log is available here:']);
sheet_.appendRow([oldLog.getUrl()]);
//release lock
lock.releaseLock();
}
}
//logs to spreadsheet
function logToSheet_(msg, level) {
//check for rollover every 100 rows logged during one invocation
if (counter % 100 === 0) {
rollLogOver_();
}
var message = convertUsingSheetPatternLayout_(msg, level);
if (hasAuth_()) {
call_(function() {sheet_.appendRow([message]);});
}
}
// convert message to text string
function convertUsingDefaultPatternLayout_(msg, level) {
var now = new Date;
var dt = Utilities.formatDate(now, Session.getScriptTimeZone(), DATE_TIME_LAYOUT);
var message = dt + " " + pad_(now - START_TIME, 6) + " " + levelToString_(level) + " " + msg;
return message;
}
// convert message to text string
function convertUsingSheetPatternLayout_(msg, level) {
return convertUsingDefaultPatternLayout_(msg, level);
}
//Sets the log sheet, creating one if it doesn't exist
function setLogSheet_(optKey, optSheetName) {
var sheetName = optSheetName || "Log";
var ss = (optKey) ? SpreadsheetApp.openById(optKey) : SpreadsheetApp.getActiveSpreadsheet();
var sheets = call_(function() {return ss.getSheets();});
for (var i = 0; i < sheets.length; i++) {
if (sheets[i].getName() === sheetName) {
sheet_ = sheets[i];
return;
}
}
sheet_ = ss.insertSheet(sheetName, i);
sheet_.deleteColumns(2,sheet_.getMaxColumns()-1);
sheet_.getRange(1,1).setValue(SHEET_LOG_HEADER);
sheet_.setFrozenRows(1);
sheet_.setColumnWidth(1, SHEET_LOG_CELL_WIDTH);
info("Log created");
}
// pads a number with leading zeros
function pad_(n,len) {
var s = n.toString();
if (s.length < len) {
s = ('0000000000' + s).slice(-len);
}
return s;
}
function test(message, optValues) {
var lev = Level.INFO;
if (isLoggable_(lev)) {
log_(arguments, lev);
}
return thisApp_;
}
//copy version 10 lib GASRetry 'MGJu3PS2ZYnANtJ9kyn2vnlLDhaBgl_dE' (changed function name and log line)
function call_(func, optLoggerFunction) {
for (var n=0; n<6; n++) {
try {
return func();
} catch(e) {
if (optLoggerFunction) {optLoggerFunction("call_ " + n + ": " + e)}
if (n == 5) {
throw e;
}
Utilities.sleep((Math.pow(2,n)*1000) + (Math.round(Math.random() * 1000)));
}
}
}