From 60a8ec74760dd01e79a3176e3aadde0a9b14e6bb Mon Sep 17 00:00:00 2001 From: Gandalf Date: Thu, 11 Dec 2025 00:19:03 +0100 Subject: [PATCH] Add debug mode for logging server-side API calls Implements issue #762: Add debug logging for all Hive API calls to help identify excessive calls and debug production issues. Features: - Logs all wax API calls (bridge, condenser_api, database_api, etc.) - Logs third-party API calls (hiveposh, hivebuzz, peakd) - Configurable via environment variables - No performance impact when disabled Configuration: - DEBUG_API_CALLS=true Enable API call logging - DEBUG_API_LEVEL=all Log level: all, slow, errors - DEBUG_API_SLOW_THRESHOLD=1000 Slow call threshold (ms) Log format (single-line JSON): {"level":"INFO","api":"bridge.get_post","params":{...},"status":"success","duration_ms":150} Closes #762 --- packages/transaction/lib/api-logger.ts | 51 ++++++++++ packages/transaction/lib/chain-proxy.ts | 82 ++++++++++++++++ packages/transaction/lib/chain.ts | 6 +- packages/transaction/lib/custom-api.ts | 121 +++++++++++++++++++----- 4 files changed, 233 insertions(+), 27 deletions(-) create mode 100644 packages/transaction/lib/api-logger.ts create mode 100644 packages/transaction/lib/chain-proxy.ts diff --git a/packages/transaction/lib/api-logger.ts b/packages/transaction/lib/api-logger.ts new file mode 100644 index 000000000..19ffcb901 --- /dev/null +++ b/packages/transaction/lib/api-logger.ts @@ -0,0 +1,51 @@ +import { getLogger } from '@ui/lib/logging'; + +const logger = getLogger('api-calls'); + +const isServer = typeof window === 'undefined'; + +// Only read env vars on server side +const DEBUG_API_CALLS = isServer ? process.env.DEBUG_API_CALLS === 'true' : false; +const DEBUG_API_LEVEL = isServer ? process.env.DEBUG_API_LEVEL || 'all' : 'all'; +const DEBUG_API_SLOW_THRESHOLD = isServer + ? parseInt(process.env.DEBUG_API_SLOW_THRESHOLD || '1000', 10) + : 1000; + +export interface ApiCallLog { + api: string; + params: unknown; + status: 'success' | 'error'; + duration_ms: number; + error?: string; +} + +export function isApiLoggingEnabled(): boolean { + return DEBUG_API_CALLS; +} + +export function logApiCall(log: ApiCallLog): void { + if (!DEBUG_API_CALLS) return; + + const shouldLog = + DEBUG_API_LEVEL === 'all' || + (DEBUG_API_LEVEL === 'slow' && log.duration_ms >= DEBUG_API_SLOW_THRESHOLD) || + (DEBUG_API_LEVEL === 'errors' && log.status === 'error'); + + if (!shouldLog) return; + + const logData = { + api: log.api, + params: log.params, + status: log.status, + duration_ms: log.duration_ms, + ...(log.error && { error: log.error }) + }; + + if (log.status === 'error') { + logger.error(logData, 'API call failed'); + } else if (log.duration_ms >= DEBUG_API_SLOW_THRESHOLD) { + logger.warn(logData, 'Slow API call'); + } else { + logger.info(logData, 'API call'); + } +} diff --git a/packages/transaction/lib/chain-proxy.ts b/packages/transaction/lib/chain-proxy.ts new file mode 100644 index 000000000..1e23cf024 --- /dev/null +++ b/packages/transaction/lib/chain-proxy.ts @@ -0,0 +1,82 @@ +import { logApiCall, isApiLoggingEnabled } from './api-logger'; + +/** + * Creates a proxy that intercepts API calls and logs them. + * Recursively wraps nested objects to handle namespaced APIs like + * chain.api.bridge.get_post() or chain.restApi['hivesense-api'].posts.search() + */ +function createLoggingProxy(target: T, pathPrefix: string): T { + return new Proxy(target, { + get(obj, prop) { + const value = Reflect.get(obj, prop); + const propName = String(prop); + const currentPath = pathPrefix ? `${pathPrefix}.${propName}` : propName; + + // Skip internal properties and symbols + if (typeof prop === 'symbol' || propName.startsWith('_')) { + return value; + } + + // If it's a function, wrap it with logging + if (typeof value === 'function') { + return async (...args: unknown[]) => { + const start = Date.now(); + + try { + const result = await value.apply(obj, args); + logApiCall({ + api: currentPath, + params: args.length === 1 ? args[0] : args, + status: 'success', + duration_ms: Date.now() - start + }); + return result; + } catch (error) { + logApiCall({ + api: currentPath, + params: args.length === 1 ? args[0] : args, + status: 'error', + duration_ms: Date.now() - start, + error: error instanceof Error ? error.message : String(error) + }); + throw error; + } + }; + } + + // For nested objects, recursively wrap them + if (typeof value === 'object' && value !== null) { + return createLoggingProxy(value as object, currentPath); + } + + return value; + } + }); +} + +/** + * Wraps a HiveChain instance with logging proxies on api and restApi properties. + * Only wraps if DEBUG_API_CALLS is enabled. + */ +export function wrapChainWithLogging(chain: T): T { + if (!isApiLoggingEnabled()) { + return chain; + } + + // Create a proxy for the chain that intercepts api and restApi access + return new Proxy(chain, { + get(obj, prop) { + const value = Reflect.get(obj, prop); + + if (prop === 'api' && typeof value === 'object' && value !== null) { + return createLoggingProxy(value as object, ''); + } + + if (prop === 'restApi' && typeof value === 'object' && value !== null) { + return createLoggingProxy(value as object, 'rest'); + } + + return value; + } + }); +} diff --git a/packages/transaction/lib/chain.ts b/packages/transaction/lib/chain.ts index a3f23bcac..4ca73d339 100644 --- a/packages/transaction/lib/chain.ts +++ b/packages/transaction/lib/chain.ts @@ -1,6 +1,7 @@ import type { ExtendedNodeApi, ExtendedRestApi } from './extended-hive.chain'; import { hiveChainService } from './hive-chain-service'; import { TWaxExtended, TWaxRestExtended } from '@hiveio/wax'; +import { wrapChainWithLogging } from './chain-proxy'; import pLimit from 'p-limit'; let chain: TWaxExtended> | undefined = undefined; @@ -13,7 +14,10 @@ const wasmLock = pLimit(1); export async function getChain() { return wasmLock(async () => { - if (!chain) chain = await hiveChainService.getHiveChain(); + if (!chain) { + const rawChain = await hiveChainService.getHiveChain(); + chain = wrapChainWithLogging(rawChain); + } return chain; }); } diff --git a/packages/transaction/lib/custom-api.ts b/packages/transaction/lib/custom-api.ts index 57b534345..42f93d147 100644 --- a/packages/transaction/lib/custom-api.ts +++ b/packages/transaction/lib/custom-api.ts @@ -1,38 +1,107 @@ +import { logApiCall } from './api-logger'; + export const getTwitterInfo = async (username: string) => { - const response = await fetch(`https://hiveposh.com/api/v0/twitter/${username}`, { cache: 'no-store' }); - if (!response.ok) { - throw new Error(`Posh API Error: ${response.status}`); - } + const start = Date.now(); + const api = 'hiveposh.twitter'; - const data = await response.json(); - const { error } = data; - if (error) { - throw new Error(`Posh API Error: ${error}`); - } + try { + const response = await fetch(`https://hiveposh.com/api/v0/twitter/${username}`, { cache: 'no-store' }); + if (!response.ok) { + throw new Error(`Posh API Error: ${response.status}`); + } + + const data = await response.json(); + const { error } = data; + if (error) { + throw new Error(`Posh API Error: ${error}`); + } - return data; + logApiCall({ + api, + params: { username }, + status: 'success', + duration_ms: Date.now() - start + }); + + return data; + } catch (error) { + logApiCall({ + api, + params: { username }, + status: 'error', + duration_ms: Date.now() - start, + error: error instanceof Error ? error.message : String(error) + }); + throw error; + } }; export const getHivebuzzBadges = async (username: string) => { - const response = await fetch(`https://hivebuzz.me/api/badges/${username}`, { cache: 'no-store' }); - if (!response.ok) { - throw new Error(`Hivebuzz API Error: ${response.status}`); - } + const start = Date.now(); + const api = 'hivebuzz.badges'; + + try { + const response = await fetch(`https://hivebuzz.me/api/badges/${username}`, { cache: 'no-store' }); + if (!response.ok) { + throw new Error(`Hivebuzz API Error: ${response.status}`); + } - const data = await response.json(); - return data.filter((badge: { state: string }) => badge.state === 'on'); + const data = await response.json(); + const result = data.filter((badge: { state: string }) => badge.state === 'on'); + + logApiCall({ + api, + params: { username }, + status: 'success', + duration_ms: Date.now() - start + }); + + return result; + } catch (error) { + logApiCall({ + api, + params: { username }, + status: 'error', + duration_ms: Date.now() - start, + error: error instanceof Error ? error.message : String(error) + }); + throw error; + } }; export const getPeakdBadges = async (username: string) => { - const response = await fetch(`https://peakd.com/api/public/badge/${username}`, { cache: 'no-store' }); - if (!response.ok) { - throw new Error(`Peakd API Error: ${response.status}`); - } + const start = Date.now(); + const api = 'peakd.badges'; - const data = await response.json(); - return data.map((obj: { name: string; title: string }) => ({ - id: obj.title, - name: obj.name, - title: obj.title - })); + try { + const response = await fetch(`https://peakd.com/api/public/badge/${username}`, { cache: 'no-store' }); + if (!response.ok) { + throw new Error(`Peakd API Error: ${response.status}`); + } + + const data = await response.json(); + const result = data.map((obj: { name: string; title: string }) => ({ + id: obj.title, + name: obj.name, + title: obj.title + })); + + logApiCall({ + api, + params: { username }, + status: 'success', + duration_ms: Date.now() - start + }); + + return result; + } catch (error) { + logApiCall({ + api, + params: { username }, + status: 'error', + duration_ms: Date.now() - start, + error: error instanceof Error ? error.message : String(error) + }); + throw error; + } }; -- GitLab