mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Better logging in DocTriggers
Summary: Added a helper to include lots of metadata in every logging call, added and converted many logging calls. Test Plan: Existing tests pass Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D3136
This commit is contained in:
parent
e09e919016
commit
561e32fb44
@ -61,9 +61,8 @@ export interface WebHookSecret {
|
|||||||
|
|
||||||
// Work to do after fetching values from the document
|
// Work to do after fetching values from the document
|
||||||
interface Task {
|
interface Task {
|
||||||
tableId: string;
|
|
||||||
tableDelta: TableDelta;
|
tableDelta: TableDelta;
|
||||||
triggers: any;
|
triggers: Trigger[];
|
||||||
tableDataAction: Promise<TableDataAction>;
|
tableDataAction: Promise<TableDataAction>;
|
||||||
recordDeltas: RecordDeltas;
|
recordDeltas: RecordDeltas;
|
||||||
}
|
}
|
||||||
@ -178,7 +177,7 @@ export class DocTriggers {
|
|||||||
// Fetch the modified records in full so they can be sent in webhooks
|
// Fetch the modified records in full so they can be sent in webhooks
|
||||||
// They will also be used to check if the record is ready
|
// They will also be used to check if the record is ready
|
||||||
const tableDataAction = this._activeDoc.fetchQuery(docSession, {tableId, filters});
|
const tableDataAction = this._activeDoc.fetchQuery(docSession, {tableId, filters});
|
||||||
tasks.push({tableId, tableDelta, triggers, tableDataAction, recordDeltas});
|
tasks.push({tableDelta, triggers, tableDataAction, recordDeltas});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,6 +191,7 @@ export class DocTriggers {
|
|||||||
if (!events.length) {
|
if (!events.length) {
|
||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
|
this._log("Total number of webhook events generated by bundle", {numEvents: events.length});
|
||||||
|
|
||||||
// Only add events to the queue after we finish fetching the backup from redis
|
// Only add events to the queue after we finish fetching the backup from redis
|
||||||
// to ensure that events are delivered in the order they were generated.
|
// to ensure that events are delivered in the order they were generated.
|
||||||
@ -225,6 +225,18 @@ export class DocTriggers {
|
|||||||
return this._webHookEventQueue.length >= MAX_QUEUE_SIZE;
|
return this._webHookEventQueue.length >= MAX_QUEUE_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _log(msg: string, {level = 'info', ...meta}: any = {}) {
|
||||||
|
log.origLog(level, 'DocTriggers: ' + msg, {
|
||||||
|
...meta,
|
||||||
|
docId: this._docId,
|
||||||
|
queueLength: this._webHookEventQueue.length,
|
||||||
|
drainingQueue: this._drainingQueue,
|
||||||
|
shuttingDown: this._shuttingDown,
|
||||||
|
sending: this._sending,
|
||||||
|
redisClient: Boolean(this._redisClientField),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async _pushToRedisQueue(events: WebHookEvent[]) {
|
private async _pushToRedisQueue(events: WebHookEvent[]) {
|
||||||
const strings = events.map(e => JSON.stringify(e));
|
const strings = events.map(e => JSON.stringify(e));
|
||||||
await this._redisClient!.rpushAsync(this._redisQueueKey, ...strings);
|
await this._redisClient!.rpushAsync(this._redisQueueKey, ...strings);
|
||||||
@ -233,6 +245,7 @@ export class DocTriggers {
|
|||||||
private async _getRedisQueue(redisClient: RedisClient) {
|
private async _getRedisQueue(redisClient: RedisClient) {
|
||||||
const strings = await redisClient.lrangeAsync(this._redisQueueKey, 0, -1);
|
const strings = await redisClient.lrangeAsync(this._redisQueueKey, 0, -1);
|
||||||
if (strings.length) {
|
if (strings.length) {
|
||||||
|
this._log("Webhook events found on redis queue", {numEvents: strings.length});
|
||||||
const events = strings.map(s => JSON.parse(s));
|
const events = strings.map(s => JSON.parse(s));
|
||||||
this._webHookEventQueue.unshift(...events);
|
this._webHookEventQueue.unshift(...events);
|
||||||
this._startSendLoop();
|
this._startSendLoop();
|
||||||
@ -257,12 +270,13 @@ export class DocTriggers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _handleTask(
|
private _handleTask(
|
||||||
{tableDelta, tableId, triggers, recordDeltas}: Task,
|
{tableDelta, triggers, recordDeltas}: Task,
|
||||||
tableDataAction: TableDataAction,
|
tableDataAction: TableDataAction,
|
||||||
) {
|
) {
|
||||||
const bulkColValues = fromTableDataAction(tableDataAction);
|
const bulkColValues = fromTableDataAction(tableDataAction);
|
||||||
|
|
||||||
log.info(`Processing ${triggers.length} triggers for ${bulkColValues.id.length} records of ${tableId}`);
|
const meta = {numTriggers: triggers.length, numRecords: bulkColValues.id.length};
|
||||||
|
this._log(`Processing triggers`, meta);
|
||||||
|
|
||||||
const makePayload = _.memoize((rowIndex: number) =>
|
const makePayload = _.memoize((rowIndex: number) =>
|
||||||
_.mapValues(bulkColValues, col => col[rowIndex]) as RowRecord
|
_.mapValues(bulkColValues, col => col[rowIndex]) as RowRecord
|
||||||
@ -291,6 +305,9 @@ export class DocTriggers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._log("Generated events from triggers", {numEvents: result.length, ...meta});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -334,7 +351,7 @@ export class DocTriggers {
|
|||||||
} else if (deltaBefore === "?") {
|
} else if (deltaBefore === "?") {
|
||||||
// The ActionSummary shouldn't contain this kind of delta at all
|
// The ActionSummary shouldn't contain this kind of delta at all
|
||||||
// since it comes from a single action bundle, not a combination of summaries.
|
// since it comes from a single action bundle, not a combination of summaries.
|
||||||
log.warn('Unexpected deltaBefore === "?"', {trigger, isReadyColId, rowId, docId: this._activeDoc.docName});
|
this._log('Unexpected deltaBefore === "?"', {level: 'warn', trigger});
|
||||||
readyBefore = true;
|
readyBefore = true;
|
||||||
} else {
|
} else {
|
||||||
// Only remaining case is that deltaBefore is a single-element array containing the previous value.
|
// Only remaining case is that deltaBefore is a single-element array containing the previous value.
|
||||||
@ -365,8 +382,9 @@ export class DocTriggers {
|
|||||||
private async _getWebHookUrl(id: string): Promise<string | undefined> {
|
private async _getWebHookUrl(id: string): Promise<string | undefined> {
|
||||||
let webhook = this._webhookCache.get(id);
|
let webhook = this._webhookCache.get(id);
|
||||||
if (!webhook) {
|
if (!webhook) {
|
||||||
const secret = await this._activeDoc.getHomeDbManager()?.getSecret(id, this._activeDoc.docName);
|
const secret = await this._activeDoc.getHomeDbManager()?.getSecret(id, this._docId);
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
|
this._log(`No webhook secret found`, {level: 'warn', id});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
webhook = JSON.parse(secret);
|
webhook = JSON.parse(secret);
|
||||||
@ -374,7 +392,7 @@ export class DocTriggers {
|
|||||||
}
|
}
|
||||||
const url = webhook!.url;
|
const url = webhook!.url;
|
||||||
if (!isUrlAllowed(url)) {
|
if (!isUrlAllowed(url)) {
|
||||||
log.warn(`Webhook not sent to forbidden URL: ${url}`);
|
this._log(`Webhook not sent to forbidden URL`, {level: 'warn', url});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
@ -384,7 +402,7 @@ export class DocTriggers {
|
|||||||
if (!this._sending) { // only run one loop at a time
|
if (!this._sending) { // only run one loop at a time
|
||||||
this._sending = true;
|
this._sending = true;
|
||||||
this._sendLoop().catch((e) => { // run _sendLoop asynchronously (in the background)
|
this._sendLoop().catch((e) => { // run _sendLoop asynchronously (in the background)
|
||||||
log.error(`_sendLoop failed: ${e}`);
|
this._log(`_sendLoop failed: ${e}`, {level: 'error'});
|
||||||
this._sending = false; // otherwise the following line will complete instantly
|
this._sending = false; // otherwise the following line will complete instantly
|
||||||
this._startSendLoop(); // restart the loop on failure
|
this._startSendLoop(); // restart the loop on failure
|
||||||
});
|
});
|
||||||
@ -396,7 +414,7 @@ export class DocTriggers {
|
|||||||
// Managed by _startSendLoop. Runs in the background. Only one loop should run at a time.
|
// Managed by _startSendLoop. Runs in the background. Only one loop should run at a time.
|
||||||
// Runs until shutdown.
|
// Runs until shutdown.
|
||||||
private async _sendLoop() {
|
private async _sendLoop() {
|
||||||
log.info("Starting _sendLoop");
|
this._log("Starting _sendLoop");
|
||||||
|
|
||||||
// TODO delay/prevent shutting down while queue isn't empty?
|
// TODO delay/prevent shutting down while queue isn't empty?
|
||||||
while (!this._shuttingDown) {
|
while (!this._shuttingDown) {
|
||||||
@ -408,10 +426,14 @@ export class DocTriggers {
|
|||||||
const batch = _.takeWhile(this._webHookEventQueue.slice(0, 100), {id});
|
const batch = _.takeWhile(this._webHookEventQueue.slice(0, 100), {id});
|
||||||
const body = JSON.stringify(batch.map(e => e.payload));
|
const body = JSON.stringify(batch.map(e => e.payload));
|
||||||
const url = await this._getWebHookUrl(id);
|
const url = await this._getWebHookUrl(id);
|
||||||
|
let meta: Record<string, any>|undefined;
|
||||||
|
|
||||||
let success: boolean;
|
let success: boolean;
|
||||||
if (!url) {
|
if (!url) {
|
||||||
success = true;
|
success = true;
|
||||||
} else {
|
} else {
|
||||||
|
meta = {numEvents: batch.length, webhookId: id, host: new URL(url).host};
|
||||||
|
this._log("Sending batch of webhook events", meta);
|
||||||
success = await this._sendWebhookWithRetries(url, body);
|
success = await this._sendWebhookWithRetries(url, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -423,7 +445,9 @@ export class DocTriggers {
|
|||||||
multi.ltrim(this._redisQueueKey, batch.length, -1);
|
multi.ltrim(this._redisQueueKey, batch.length, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!success && !this._drainingQueue) {
|
if (!success) {
|
||||||
|
this._log("Failed to send batch of webhook events", {...meta, level: 'warn'});
|
||||||
|
if (!this._drainingQueue) {
|
||||||
// Put the failed events at the end of the queue to try again later
|
// Put the failed events at the end of the queue to try again later
|
||||||
// while giving other URLs a chance to receive events.
|
// while giving other URLs a chance to receive events.
|
||||||
this._webHookEventQueue.push(...batch);
|
this._webHookEventQueue.push(...batch);
|
||||||
@ -432,15 +456,18 @@ export class DocTriggers {
|
|||||||
multi.rpush(this._redisQueueKey, ...strings);
|
multi.rpush(this._redisQueueKey, ...strings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (meta) {
|
||||||
|
this._log("Successfully sent batch of webhook events", meta);
|
||||||
|
}
|
||||||
|
|
||||||
await multi?.execAsync();
|
await multi?.execAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Ended _sendLoop");
|
this._log("Ended _sendLoop");
|
||||||
|
|
||||||
this._redisClient?.quitAsync().catch(e =>
|
this._redisClient?.quitAsync().catch(e =>
|
||||||
// Catch error to prevent sendLoop being restarted
|
// Catch error to prevent sendLoop being restarted
|
||||||
log.warn("Error quitting redis: " + e)
|
this._log("Error quitting redis: " + e, {level: 'warn'})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -450,6 +477,7 @@ export class DocTriggers {
|
|||||||
}
|
}
|
||||||
const redisUrl = process.env.REDIS_URL;
|
const redisUrl = process.env.REDIS_URL;
|
||||||
if (redisUrl) {
|
if (redisUrl) {
|
||||||
|
this._log("Creating redis client");
|
||||||
this._redisClientField = createClient(redisUrl);
|
this._redisClientField = createClient(redisUrl);
|
||||||
}
|
}
|
||||||
return this._redisClientField;
|
return this._redisClientField;
|
||||||
@ -480,9 +508,9 @@ export class DocTriggers {
|
|||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
log.warn(`Webhook responded with status ${response.status}`);
|
this._log(`Webhook responded with non-200 status`, {level: 'warn', status: response.status, attempt});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.warn(`Webhook error: ${e}`);
|
this._log(`Webhook sending error: ${e}`, {level: 'warn', attempt});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't wait any more if this is the last attempt.
|
// Don't wait any more if this is the last attempt.
|
||||||
|
Loading…
Reference in New Issue
Block a user