-
Notifications
You must be signed in to change notification settings - Fork 426
/
logs.ts
238 lines (204 loc) Β· 7.52 KB
/
logs.ts
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
import chalk from 'chalk';
import {logging_v2 as loggingV2} from 'googleapis';
import open from 'open';
import {loadAPICredentials, logger} from '../auth.js';
import {ClaspError} from '../clasp-error.js';
import {DOTFILE, ProjectSettings} from '../dotfile.js';
import {projectIdPrompt} from '../inquirer.js';
import {ERROR, LOG} from '../messages.js';
import {URL} from '../urls.js';
import {
checkIfOnlineOrDie,
getErrorMessage,
getProjectSettings,
isValidProjectId,
spinner,
stopSpinner,
} from '../utils.js';
interface CommandOption {
readonly json?: boolean;
readonly open?: boolean;
readonly setup?: boolean;
readonly watch?: boolean;
readonly simplified?: boolean;
}
/**
* Prints StackDriver logs from this Apps Script project.
* @param options.json {boolean} If true, the command will output logs as json.
* @param options.open {boolean} If true, the command will open the StackDriver logs website.
* @param options.setup {boolean} If true, the command will help you setup logs.
* @param options.watch {boolean} If true, the command will watch for logs and print them. Exit with ^C.
* @param options.simplified {boolean} If true, the command will remove timestamps from the logs.
*/
export default async (options: CommandOption): Promise<void> => {
await checkIfOnlineOrDie();
// Get project settings.
const projectSettings = await getProjectSettings();
let projectId = options.setup ? await setupLogs(projectSettings) : projectSettings.projectId;
if (!projectId) {
console.log(LOG.NO_GCLOUD_PROJECT);
projectId = await setupLogs(projectSettings);
console.log(LOG.LOGS_SETUP);
}
// If we're opening the logs, get the URL, open, then quit.
if (options.open) {
const url = URL.LOGS(projectId);
console.log(`Opening logs: ${url}`);
await open(url, {wait: false});
return;
}
const {json, simplified} = options;
// Otherwise, if not opening StackDriver, load StackDriver logs.
if (options.watch) {
const POLL_INTERVAL = 6000; // 6s
setInterval(() => {
const startDate = new Date();
startDate.setSeconds(startDate.getSeconds() - (10 * POLL_INTERVAL) / 1000);
fetchAndPrintLogs(json, simplified, projectId, startDate);
}, POLL_INTERVAL);
} else {
await fetchAndPrintLogs(json, simplified, projectId);
}
};
/**
* This object holds all log IDs that have been printed to the user.
* This prevents log entries from being printed multiple times.
* StackDriver isn't super reliable, so it's easier to get generous chunk of logs and filter them
* rather than filter server-side.
* @see logs.data.entries[0].insertId
*/
const logEntryCache: {[key: string]: boolean} = {};
const severityColor: {[key: string]: chalk.Chalk} = {
ERROR: chalk.red,
INFO: chalk.cyan,
DEBUG: chalk.green, // Includes timeEnd
NOTICE: chalk.magenta,
WARNING: chalk.yellow,
};
/**
* Prints log entries
* @param entries {any[]} StackDriver log entries.
*/
const printLogs = (
input: ReadonlyArray<Readonly<loggingV2.Schema$LogEntry>> = [],
formatJson = false,
simplified = false
): void => {
const entries = [...input].reverse().slice(0, 50); // Print in syslog ascending order
for (const entry of entries) {
const {severity = '', timestamp = '', resource, insertId = ''} = entry;
if (resource?.labels) {
let {function_name: functionName = ERROR.NO_FUNCTION_NAME} = resource.labels;
functionName = functionName.padEnd(15);
let payloadData: string | {[key: string]: unknown} = '';
if (formatJson) {
payloadData = JSON.stringify(entry, null, 2);
} else {
const kludge = obscure(entry, functionName);
payloadData = kludge.payloadData;
functionName = kludge.functionName;
}
const coloredSeverity = `${severityColor[severity!](severity) || severity!}`.padEnd(20);
// If we haven't logged this entry before, log it and mark the cache.
if (!logEntryCache[insertId!]) {
console.log(
simplified
? `${coloredSeverity} ${functionName} ${payloadData}`
: `${coloredSeverity} ${timestamp} ${functionName} ${payloadData}`
);
logEntryCache[insertId!] = true;
}
}
}
};
const obscure = (entry: Readonly<loggingV2.Schema$LogEntry>, functionName: string) => {
const {jsonPayload, protoPayload = {}, textPayload} = entry;
// Chokes on unmatched json payloads
// jsonPayload: jsonPayload ? jsonPayload.fields.message.stringValue : '',
let payloadData =
textPayload ??
((jsonPayload ? JSON.stringify(jsonPayload).slice(0, 255) : '') || protoPayload) ??
ERROR.PAYLOAD_UNKNOWN;
if (typeof payloadData !== 'string' && protoPayload!['@type'] === 'type.googleapis.com/google.cloud.audit.AuditLog') {
payloadData = LOG.STACKDRIVER_SETUP;
functionName = (protoPayload!.methodName as string).padEnd(15);
}
if (payloadData && typeof payloadData === 'string') {
payloadData = payloadData.padEnd(20);
}
return {functionName, payloadData};
};
const setupLogs = async (projectSettings: ProjectSettings): Promise<string> => {
try {
console.log(`${LOG.OPEN_LINK(LOG.SCRIPT_LINK(projectSettings.scriptId))}\n`);
console.log(`${LOG.GET_PROJECT_ID_INSTRUCTIONS}\n`);
const dotfile = DOTFILE.PROJECT();
if (!dotfile) {
throw new ClaspError(ERROR.SETTINGS_DNE);
}
const settings = await dotfile.read<ProjectSettings>();
if (!settings.scriptId) {
throw new ClaspError(ERROR.SCRIPT_ID_DNE);
}
const {projectId} = await projectIdPrompt();
await dotfile.write({...settings, projectId});
return projectId;
} catch (error) {
if (error instanceof ClaspError) {
throw error;
}
throw new ClaspError(getErrorMessage(error) as string); // TODO get rid of type casting
}
};
/**
* Fetches the logs and prints the to the user.
* @param startDate {Date?} Get logs from this date to now.
*/
const fetchAndPrintLogs = async (
formatJson = false,
simplified = false,
projectId?: string,
startDate?: Date
): Promise<void> => {
// Validate projectId
if (!projectId) {
throw new ClaspError(ERROR.NO_GCLOUD_PROJECT);
}
if (!isValidProjectId(projectId)) {
throw new ClaspError(ERROR.PROJECT_ID_INCORRECT(projectId));
}
const {isLocalCreds} = await loadAPICredentials();
spinner.start(`${isLocalCreds ? LOG.LOCAL_CREDS : ''}${LOG.GRAB_LOGS}`);
// Create a time filter (timestamp >= "2016-11-29T23:00:00Z")
// https://cloud.google.com/logging/docs/view/advanced-filters#search-by-time
const filter = startDate ? `timestamp >= "${startDate.toISOString()}"` : '';
try {
const logs = await logger.entries.list({
requestBody: {resourceNames: [`projects/${projectId}`], filter, orderBy: 'timestamp desc'},
});
// We have an API response. Now, check the API response status.
stopSpinner();
// Only print filter if provided.
if (filter.length > 0) {
console.log(filter);
}
// Parse response and print logs or print error message.
const {data, status, statusText} = logs;
switch (status) {
case 200:
printLogs(data.entries, formatJson, simplified);
break;
case 401:
throw new ClaspError(isLocalCreds ? ERROR.UNAUTHENTICATED_LOCAL : ERROR.UNAUTHENTICATED);
case 403:
throw new ClaspError(isLocalCreds ? ERROR.PERMISSION_DENIED_LOCAL : ERROR.PERMISSION_DENIED);
default:
throw new ClaspError(`(${status}) Error: ${statusText}`);
}
} catch (error) {
if (error instanceof ClaspError) {
throw error;
}
throw new ClaspError(ERROR.PROJECT_ID_INCORRECT(projectId));
}
};