🔊 back: output statistics periodically to logs (#762)

This commit is contained in:
Eric Doughty-Papassideris
2024-12-13 19:50:37 +01:00
parent 633f8ff71b
commit f6d3072bfe
5 changed files with 66 additions and 11 deletions
@@ -21,7 +21,9 @@
"__name": "DIAG_SKIP_KEYS", "__name": "DIAG_SKIP_KEYS",
"__format": "json" "__format": "json"
}, },
"probeSecret": "DIAG_PROBE_SECRET" "probeSecret": "DIAG_PROBE_SECRET",
"statsLogPeriodMs": "DIAG_STATS_PRINT_PERIOD_MS",
"statsFullStatsLogPeriodMs": "DIAG_FULL_STATS_PRINT_PERIOD_MS"
}, },
"webserver": { "webserver": {
"host": "TWAKE_DRIVE_HOST", "host": "TWAKE_DRIVE_HOST",
+3 -1
View File
@@ -37,7 +37,9 @@
}, },
"diagnostics": { "diagnostics": {
"skipKeys": [], "skipKeys": [],
"probeSecret": "" "probeSecret": "",
"statsLogPeriodMs": 120000,
"statsFullStatsLogPeriodMs": 600000
}, },
"tracker": { "tracker": {
"type": "segment", "type": "segment",
@@ -1,6 +1,8 @@
import assert from "node:assert"; import assert from "node:assert";
import config from "../../../config"; import config from "../../../config";
import { logger } from "../logger"; import { getLogger } from "../logger";
const logger = getLogger("Diagnostics");
/** /**
* Values that can match a set of diagnostic providers. * Values that can match a set of diagnostic providers.
@@ -44,6 +46,10 @@ interface IDiagnosticsConfig {
// relied on for security because disabling diagnostics. At worst this // relied on for security because disabling diagnostics. At worst this
// provides access to the DB statistics. // provides access to the DB statistics.
probeSecret?: string; probeSecret?: string;
// Period at which to log TDiagnosticTag `stats`. 0 to disable.
statsLogPeriodMs: number;
// Period at which to log TDiagnosticTag `stats-full`. 0 to disable.
statsFullStatsLogPeriodMs: number;
} }
export const getConfig = (): IDiagnosticsConfig => { export const getConfig = (): IDiagnosticsConfig => {
@@ -56,7 +62,17 @@ export const getConfig = (): IDiagnosticsConfig => {
.split(/[,\s]+/g) .split(/[,\s]+/g)
.filter(x => !!x), .filter(x => !!x),
}; };
return configSection;
const getNumberFromConfig = (value): number => {
if (typeof value == "number") return value;
if (typeof value == "string") return parseInt(value, 10) ?? 0;
return 0;
};
return {
...configSection,
statsLogPeriodMs: getNumberFromConfig(configSection.statsLogPeriodMs),
statsFullStatsLogPeriodMs: getNumberFromConfig(configSection.statsFullStatsLogPeriodMs),
};
}; };
/** Code-wide unique key for each provider */ /** Code-wide unique key for each provider */
@@ -125,20 +141,22 @@ const recordDiagnostic = (startMs: number, key: TDiagnosticKey, data?: object, e
...(error ? { ok: false, error } : { ...data }), ...(error ? { ok: false, error } : { ...data }),
}); });
const runProvider = async provider => { const runProvider = async (provider, log) => {
const startMs = now(); const startMs = now();
try { try {
const result = await provider.get(); const result = await provider.get();
if (!result.ok) if (!result.ok)
logger.error( logger.error(
{ provider: provider.key, result }, { diagnostic: provider.key, ...result },
"Got diagnostic provider result with ok=false", "Got diagnostic provider result with ok=false",
); );
else if (result.warn) else if (result.warn)
logger.warn( logger.warn(
{ provider: provider.key, result }, { diagnostic: provider.key, ...result },
"Got diagnostic provider result with ok=true but a warning", "Got diagnostic provider result with ok=true but a warning",
); );
else if (log)
logger.info({ diagnostic: provider.key, ...result }, "Diagnostic provider result");
return recordDiagnostic(startMs, provider.key, result); return recordDiagnostic(startMs, provider.key, result);
} catch (err) { } catch (err) {
logger.error({ err, provider: provider.key }, "Failed to read diagnostic provider"); logger.error({ err, provider: provider.key }, "Failed to read diagnostic provider");
@@ -173,7 +191,7 @@ export default {
let triggerUpdate: () => void = () => undefined; // The empty function is for the linter. I love you linter <3 let triggerUpdate: () => void = () => undefined; // The empty function is for the linter. I love you linter <3
const updateProvider = (timeoutId: number) => async () => { const updateProvider = (timeoutId: number) => async () => {
forgetPendingTimeout(timeoutId); forgetPendingTimeout(timeoutId);
await runProvider(provider); await runProvider(provider, false);
triggerUpdate(); triggerUpdate();
}; };
triggerUpdate = () => pendingTimeouts.push(setTimeout(updateProvider, provider.pollPeriodMs)); triggerUpdate = () => pendingTimeouts.push(setTimeout(updateProvider, provider.pollPeriodMs));
@@ -213,9 +231,14 @@ export default {
hasShutdown = true; hasShutdown = true;
}, },
/** Return the values of all providers which include the provided tag */ /**
* Return the values of all providers which include the provided tag.
*
* @param log if `true`, print each individual log output even if succesful
*/
async get( async get(
tag: TDiagnosticTag, tag: TDiagnosticTag,
log: boolean,
): Promise<{ ok: boolean } | { [key: TDiagnosticKey]: TDiagnosticResult }> { ): Promise<{ ok: boolean } | { [key: TDiagnosticKey]: TDiagnosticResult }> {
const config = getConfig(); const config = getConfig();
const result = { ok: true }; const result = { ok: true };
@@ -230,7 +253,7 @@ export default {
immediateDiagnosticProviders.map(async provider => { immediateDiagnosticProviders.map(async provider => {
if (!isProviderIncludedInTag(tag, provider, config)) return; if (!isProviderIncludedInTag(tag, provider, config)) return;
atLeastOneCheck = true; atLeastOneCheck = true;
const providerResult = await runProvider(provider); const providerResult = await runProvider(provider, log);
if (!providerResult.ok) result.ok = false; if (!providerResult.ok) result.ok = false;
return (result[provider.key] = providerResult); return (result[provider.key] = providerResult);
}), }),
@@ -4,6 +4,10 @@ import DiagnosticsServiceAPI from "./service-provider";
import DiagnosticsServiceImpl from "./service"; import DiagnosticsServiceImpl from "./service";
import WebServerAPI from "../webserver/provider"; import WebServerAPI from "../webserver/provider";
import registerBasicProviders from "./providers"; import registerBasicProviders from "./providers";
import diagnostics, {
getConfig as getDiagnosticsGetConfig,
TDiagnosticTag,
} from "../../framework/api/diagnostics";
/** /**
* The diagnostics service exposes endpoint that are of use for operational reasons. * The diagnostics service exposes endpoint that are of use for operational reasons.
@@ -15,6 +19,8 @@ import registerBasicProviders from "./providers";
export default class DiagnosticsService extends TdriveService<DiagnosticsServiceAPI> { export default class DiagnosticsService extends TdriveService<DiagnosticsServiceAPI> {
name = "diagnostics"; name = "diagnostics";
service: DiagnosticsServiceAPI; service: DiagnosticsServiceAPI;
private runningIntervalStatsLog?: ReturnType<typeof setInterval>;
private runningIntervalStatsFullLog?: ReturnType<typeof setInterval>;
api(): DiagnosticsServiceAPI { api(): DiagnosticsServiceAPI {
return this.service; return this.service;
@@ -30,6 +36,28 @@ export default class DiagnosticsService extends TdriveService<DiagnosticsService
next(); next();
}); });
const config = getDiagnosticsGetConfig();
const printStatsToLog = (tag: TDiagnosticTag) => () => diagnostics.get(tag, true);
const startLoggingStats = (tag, periodMs) =>
periodMs && periodMs > 0 ? setInterval(printStatsToLog(tag), periodMs) : undefined;
this.runningIntervalStatsLog = startLoggingStats("stats", config.statsLogPeriodMs);
this.runningIntervalStatsFullLog = startLoggingStats(
"stats-full",
config.statsFullStatsLogPeriodMs,
);
return this;
}
public async doStop(): Promise<this> {
if (this.runningIntervalStatsLog) {
clearInterval(this.runningIntervalStatsLog);
this.runningIntervalStatsLog = null;
}
if (this.runningIntervalStatsFullLog) {
clearInterval(this.runningIntervalStatsFullLog);
this.runningIntervalStatsFullLog = null;
}
return this; return this;
} }
} }
@@ -14,7 +14,7 @@ const routes: FastifyPluginCallback = (fastify: FastifyInstance, _opts, next) =>
(request.query as { secret: string }).secret !== diagnosticsConfig.probeSecret (request.query as { secret: string }).secret !== diagnosticsConfig.probeSecret
) )
return reply.status(403).send(); return reply.status(403).send();
const results = await diagnostics.get(tag); const results = await diagnostics.get(tag, false);
if (!results.ok) reply.status(503); if (!results.ok) reply.status(503);
return reply.send(results); return reply.send(results);
}); });