Add wrangler logs command to search past logs. Let LLMs & agents read logs more easily #13232
remorses
started this conversation in
Feature Requests
Replies: 3 comments
-
|
This would be handy to allow coding agents to query logs without using the above workaround or the Cloudflare Observability MCP Server |
Beta Was this translation helpful? Give feedback.
0 replies
-
|
my workaround. run with ./get-logs.mjs <request_id> to paste into LLMs, run with ./get-logs.mjs <request_id> --json | pbcopy #!/usr/bin/env node
/**
* Fetch all Cloudflare Worker logs for a given request ID
*
* Usage: ./scripts/get-logs.mjs <request-id> [--hours=N] [--json]
*
* Environment variables required:
* CLOUDFLARE_API_TOKEN - API token with Workers:Read permission
* CLOUDFLARE_ACCOUNT_ID - Your Cloudflare account ID
*
* Examples:
* ./scripts/get-logs.mjs 9b9b9937bc6dc65e
* ./scripts/get-logs.mjs 9b9b9937bc6dc65e --hours=24
* ./scripts/get-logs.mjs 9b9b9937bc6dc65e --json
*/
const ACCOUNT_ID = process.env.CLOUDFLARE_ACCOUNT_ID
const API_TOKEN = process.env.CLOUDFLARE_API_TOKEN
const args = process.argv.slice(2);
const requestId = args.find((arg) => !arg.startsWith('--'));
const hoursArg = args.find((arg) => arg.startsWith('--hours='));
const hours = hoursArg ? parseInt(hoursArg.split('=')[1], 10) : 168; // Default 7 days (max retention)
const jsonOutput = args.includes('--json');
if (!requestId) {
console.error('Usage: get-logs.mjs <request-id> [--hours=N] [--json]');
console.error('');
console.error('Options:');
console.error(' --hours=N Look back N hours (default: 168, max 7 days)');
console.error(' --json Output raw JSON instead of formatted logs');
console.error('');
console.error('Environment variables:');
console.error(' CLOUDFLARE_API_TOKEN - API token with Workers:Read permission');
console.error(' CLOUDFLARE_ACCOUNT_ID - Your Cloudflare account ID');
process.exit(1);
}
if (!ACCOUNT_ID || !API_TOKEN) {
console.error('Error: Missing required environment variables');
console.error(' CLOUDFLARE_ACCOUNT_ID:', ACCOUNT_ID ? 'set' : 'missing');
console.error(' CLOUDFLARE_API_TOKEN:', API_TOKEN ? 'set' : 'missing');
process.exit(1);
}
const colors = {
reset: '\x1b[0m',
dim: '\x1b[2m',
red: '\x1b[31m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
gray: '\x1b[90m',
};
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
function colorize(text, color) {
return useColor ? `${color}${text}${colors.reset}` : text;
}
function levelColor(level) {
switch (level?.toLowerCase()) {
case 'error':
return colors.red;
case 'warn':
return colors.yellow;
case 'info':
return colors.blue;
case 'debug':
return colors.gray;
default:
return colors.cyan;
}
}
function formatTimestamp(ts) {
if (!ts) return '';
const date = new Date(ts);
return date.toISOString().replace('T', ' ').replace('Z', '');
}
function truncate(str, maxLen = 200) {
if (!str || str.length <= maxLen) return str;
return str.slice(0, maxLen - 3) + '...';
}
function formatLogEntry(entry) {
const meta = entry.$metadata || {};
const workers = entry.$workers || {};
const timestamp = formatTimestamp(entry.timestamp);
const level = (meta.level || 'log').toUpperCase().padEnd(5);
const service = meta.service || workers.scriptName || '';
const message = meta.message || '';
const parts = [
colorize(timestamp, colors.dim),
colorize(level, levelColor(meta.level)),
service ? colorize(`[${service}]`, colors.magenta) : '',
message,
].filter(Boolean);
let output = parts.join(' ');
// Add context info if present
const contextKeys = Object.keys(entry).filter(
(k) => !['$metadata', '$workers', 'dataset', 'timestamp', 'source'].includes(k),
);
if (contextKeys.length > 0) {
const context = contextKeys
.map((k) => {
const val = entry[k];
if (typeof val === 'object') {
return `${k}=${truncate(JSON.stringify(val), 100)}`;
}
return `${k}=${truncate(String(val), 100)}`;
})
.join(' ');
if (context) {
output += ` ${colorize('|', colors.gray)} ${colorize(context, colors.gray)}`;
}
}
return output;
}
async function fetchLogs(requestId, hours) {
const now = new Date();
const from = new Date(now.getTime() - hours * 60 * 60 * 1000);
const query = {
view: 'events',
queryId: `logs-${requestId}`,
limit: 1000,
parameters: {
filters: [
{
key: '$metadata.requestId',
operation: 'eq',
type: 'string',
value: requestId,
},
],
},
timeframe: {
from: from.getTime(),
to: now.getTime(),
},
};
const url = `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/workers/observability/telemetry/query`;
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(query),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`API request failed: ${response.status} ${response.statusText}\n${text}`);
}
const data = await response.json();
if (!data.success) {
throw new Error(`API error: ${JSON.stringify(data.errors)}`);
}
// API returns { result: { events: { events: [...] } } }
return data.result?.events?.events || [];
}
async function main() {
try {
console.error(
colorize(`Fetching logs for request ID: ${requestId} (last ${hours} hours)...`, colors.dim),
);
const logs = await fetchLogs(requestId, hours);
if (logs.length === 0) {
console.error(colorize('No logs found for this request ID', colors.yellow));
process.exit(0);
}
// Sort by timestamp ascending
logs.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
console.error(colorize(`Found ${logs.length} log entries\n`, colors.dim));
if (jsonOutput) {
console.log(JSON.stringify(logs, null, 2));
} else {
for (const entry of logs) {
console.log(formatLogEntry(entry));
}
}
// Print summary
const meta = logs[0]?.$metadata || {};
const workers = logs[0]?.$workers || {};
console.error('');
console.error(colorize('--- Summary ---', colors.dim));
console.error(
colorize(` Service: ${meta.service || workers.scriptName || 'unknown'}`, colors.dim),
);
console.error(colorize(` Trigger: ${meta.trigger || 'unknown'}`, colors.dim));
console.error(colorize(` Entries: ${logs.length}`, colors.dim));
if (workers.outcome) {
console.error(colorize(` Outcome: ${workers.outcome}`, colors.dim));
}
} catch (error) {
console.error(colorize(`Error: ${error.message}`, colors.red));
process.exit(1);
}
}
main(); |
Beta Was this translation helpful? Give feedback.
0 replies
-
|
I struggled a lot and codex was never really able to read worker logs, so I made a solution now so AI can help fix bugs in production |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Describe the solution
With the new observability features it should be possible to implement a command that gets the last worker logs without using tail
This would be super useful for agents to let them see the logs inside the worker after a request
The API call would look something like this
Beta Was this translation helpful? Give feedback.
All reactions