@jessitronica/honeycomb-trace-spans
Version:
A command-line tool to retrieve all spans in a trace from Honeycomb
390 lines (388 loc) • 16.7 kB
JavaScript
/**
* Standalone script to retrieve all spans in a trace from Honeycomb
*
* Usage:
* honeycomb-trace-spans --trace-id <trace_id> [--time-range <seconds>]
* honeycomb-trace-spans --trace-id <trace_id> --start-time <unix_timestamp> --end-time <unix_timestamp>
*
* Examples:
* honeycomb-trace-spans --trace-id abcd1234
* honeycomb-trace-spans --trace-id abcd1234 --time-range 86400
* honeycomb-trace-spans --trace-id abcd1234 --start-time 1617235200 --end-time 1617321600
*/
// Parse command line arguments
function parseArgs() {
const args = process.argv.slice(2);
const params = {};
for (let i = 0; i < args.length; i += 2) {
const arg = args[i];
const nextArg = args[i + 1];
if (arg && nextArg) {
const key = arg.replace(/^--/, "");
params[key] = nextArg;
}
}
// Validate required parameters
if (!params["trace-id"]) {
console.error("Error: Missing required parameter --trace-id");
showUsage();
process.exit(1);
}
// Parse time parameters
let timeRange = params["time-range"]
? parseInt(params["time-range"], 10)
: 3600; // Default to 1 hour
let startTime = params["start-time"]
? parseInt(params["start-time"], 10)
: undefined;
let endTime = params["end-time"]
? parseInt(params["end-time"], 10)
: undefined;
// Validate that if one of start-time or end-time is provided, both must be provided
if ((startTime && !endTime) || (!startTime && endTime)) {
console.error("Error: Both --start-time and --end-time must be provided together");
showUsage();
process.exit(1);
}
return {
traceId: params["trace-id"],
timeRange: timeRange,
startTime: startTime,
endTime: endTime,
};
}
// Show usage information
function showUsage() {
console.log(`
Usage:
honeycomb-trace-spans --trace-id <trace_id> [--time-range <seconds>]
honeycomb-trace-spans --trace-id <trace_id> --start-time <unix_timestamp> --end-time <unix_timestamp>
Examples:
honeycomb-trace-spans --trace-id abcd1234
honeycomb-trace-spans --trace-id abcd1234 --time-range 86400
honeycomb-trace-spans --trace-id abcd1234 --start-time 1617235200 --end-time 1617321600
`);
}
// Main function
async function main() {
try {
// Parse command line arguments
const { traceId, timeRange, startTime, endTime } = parseArgs();
// Always use __all__ as the dataset to query across all datasets
const dataset = "__all__";
// Check for API key
const apiKey = process.env.HONEYCOMB_API_KEY;
if (!apiKey) {
console.error("Error: HONEYCOMB_API_KEY environment variable is not set");
process.exit(1);
}
const apiEndpoint = process.env.HONEYCOMB_API_ENDPOINT || "https://api.honeycomb.io";
// Log what time range we're using
if (startTime && endTime) {
const startDate = new Date(startTime * 1000).toISOString();
const endDate = new Date(endTime * 1000).toISOString();
console.log(`Retrieving spans for trace ${traceId} across all datasets from ${startDate} to ${endDate}...`);
}
else {
console.log(`Retrieving spans for trace ${traceId} across all datasets (last ${timeRange} seconds)...`);
}
// Create query to retrieve all spans in the trace
const query = {
calculations: [{ op: "COUNT" }],
filters: [{ column: "trace.trace_id", op: "=", value: traceId }],
breakdowns: [
"trace.span_id",
"name",
"trace.parent_id",
"service.name",
"duration_ms",
],
limit: 1000, // Set a high limit to get all spans
};
// Add time parameters to the query
if (startTime && endTime) {
// Use absolute time range
query.start_time = startTime;
query.end_time = endTime;
}
else {
// Use relative time range
query.time_range = timeRange;
}
// Create the query
const queryResponse = await request(apiKey, apiEndpoint, `/1/queries/${dataset}`, {
method: "POST",
body: JSON.stringify(query),
});
const queryId = queryResponse.id;
// Create query result
const queryResultResponse = await request(apiKey, apiEndpoint, `/1/query_results/${dataset}`, {
method: "POST",
body: JSON.stringify({ query_id: queryId }),
});
const queryResultId = queryResultResponse.id;
// Poll for results
let attempts = 0;
const maxAttempts = 10;
let result = null;
while (attempts < maxAttempts) {
const response = await request(apiKey, apiEndpoint, `/1/query_results/${dataset}/${queryResultId}`);
if (response.data?.results) {
result = response;
break;
}
// Wait before trying again
await new Promise((resolve) => setTimeout(resolve, 1000));
attempts++;
}
if (!result) {
throw new Error("Query timed out");
}
// Check if we got results
if (!result.data?.results || result.data.results.length === 0) {
// Get team info and environment name for UI link
const authInfo = await request(apiKey, apiEndpoint, "/1/auth");
const teamSlug = authInfo.team?.slug;
const envName = authInfo.environment?.name || "unknown";
if (teamSlug) {
// Create link to view the trace in Honeycomb UI
let traceUrl = `https://ui.honeycomb.io/${teamSlug}/environments/${envName}/trace?trace_id=${encodeURIComponent(traceId)}`;
// Add time range parameters to the URL
if (startTime && endTime) {
traceUrl += `&trace_start_ts=${startTime}&trace_end_ts=${endTime}`;
}
else if (timeRange) {
const endTimeSec = Math.floor(Date.now() / 1000);
const startTimeSec = endTimeSec - timeRange;
traceUrl += `&trace_start_ts=${startTimeSec}&trace_end_ts=${endTimeSec}`;
}
// Output the trace URL separately for human use
console.log(`\nView trace in Honeycomb UI: ${traceUrl}`);
// Output structured JSON for agent use
console.log(JSON.stringify({
summary: {
traceId,
spanCount: 0,
services: [],
rootSpans: 0,
timeRange: {
start: startTime || Math.floor(Date.now() / 1000) - timeRange,
end: endTime || Math.floor(Date.now() / 1000),
},
traceUrl: traceUrl,
},
spans: [],
}, null, 2));
}
else {
console.log(`No spans found for trace ID: ${traceId}`);
}
return;
}
// Check if the only result is a COUNT of 0
if (result.data.results.length === 1) {
const firstResult = result.data.results[0];
if (firstResult &&
typeof firstResult === "object" &&
"data" in firstResult &&
firstResult.data &&
typeof firstResult.data === "object" &&
"COUNT" in firstResult.data &&
firstResult.data.COUNT === 0) {
// Get team info and environment name for UI link
const authInfo = await request(apiKey, apiEndpoint, "/1/auth");
const teamSlug = authInfo.team?.slug;
const envName = authInfo.environment?.name || "unknown";
if (teamSlug) {
// Create link to view the trace in Honeycomb UI
let traceUrl = `https://ui.honeycomb.io/${teamSlug}/environments/${envName}/trace?trace_id=${encodeURIComponent(traceId)}`;
// Add time range parameters to the URL
if (startTime && endTime) {
traceUrl += `&trace_start_ts=${startTime}&trace_end_ts=${endTime}`;
}
else if (timeRange) {
const endTimeSec = Math.floor(Date.now() / 1000);
const startTimeSec = endTimeSec - timeRange;
traceUrl += `&trace_start_ts=${startTimeSec}&trace_end_ts=${endTimeSec}`;
}
// Output the trace URL separately for human use
console.log(`\nView trace in Honeycomb UI: ${traceUrl}`);
// Output structured JSON for agent use
console.log(JSON.stringify({
summary: {
traceId,
spanCount: 0,
services: [],
rootSpans: 0,
timeRange: {
start: startTime || Math.floor(Date.now() / 1000) - timeRange,
end: endTime || Math.floor(Date.now() / 1000),
},
traceUrl: traceUrl,
},
spans: [],
}, null, 2));
}
else {
console.log(`No spans found for trace ID: ${traceId}`);
}
return;
}
}
// Process the results to extract the actual span data
const processedSpans = result.data.results.map((span) => {
// Check if the span data is nested inside a 'data' property
if (span.data && typeof span.data === "object") {
return span.data;
}
return span;
});
// Sort spans by parent_id to help visualize the trace structure
const sortedSpans = [...processedSpans].sort((a, b) => {
const aParentId = String(a["trace.parent_id"] || a["parent_id"] || "");
const bParentId = String(b["trace.parent_id"] || b["parent_id"] || "");
return aParentId.localeCompare(bParentId);
});
// Create a structured representation of the spans
const structuredSpans = sortedSpans.map((span) => {
// Try different field names that might be used for the same concept
const spanId = span["trace.span_id"] || span["span.id"] || span["span_id"] || "N/A";
const parentId = span["trace.parent_id"] ||
span["parent.id"] ||
span["parent_id"] ||
"ROOT";
const name = span["name"] || span["span.name"] || "N/A";
const service = span["service.name"] || span["service"] || "N/A";
const duration = span["duration_ms"] || span["duration"] || "N/A";
// Create a structured object with the span data
const structuredSpan = {
spanId,
parentId,
name,
service,
duration,
// Include all other attributes from the original span
attributes: {},
};
// Add all other attributes to the attributes object
for (const [key, value] of Object.entries(span)) {
if (![
"trace.span_id",
"span.id",
"span_id",
"trace.parent_id",
"parent.id",
"parent_id",
"name",
"span.name",
"service.name",
"service",
"duration_ms",
"duration",
"COUNT", // Skip the COUNT field
].includes(key)) {
structuredSpan.attributes[key] = value;
}
}
return structuredSpan;
});
// We'll output the JSON data after getting the trace URL
// Get team info and environment name for UI link
const authInfo = await request(apiKey, apiEndpoint, "/1/auth");
const teamSlug = authInfo.team?.slug;
const envName = authInfo.environment?.name || "unknown";
if (teamSlug) {
// Create link to view the trace in Honeycomb UI
let traceUrl = `https://ui.honeycomb.io/${teamSlug}/environments/${envName}/trace?trace_id=${encodeURIComponent(traceId)}`;
// Add time range parameters to the URL
if (startTime && endTime) {
// Use seconds since epoch for the UI
traceUrl += `&trace_start_ts=${startTime}&trace_end_ts=${endTime}`;
}
else if (timeRange) {
// Calculate start and end times based on the relative time range
const endTimeSec = Math.floor(Date.now() / 1000);
const startTimeSec = endTimeSec - timeRange;
traceUrl += `&trace_start_ts=${startTimeSec}&trace_end_ts=${endTimeSec}`;
}
// Output the trace URL separately for human use
console.log(`\nView trace in Honeycomb UI: ${traceUrl}`);
// Also include the trace URL in the JSON output for agent use
const outputData = {
summary: {
traceId,
spanCount: structuredSpans.length,
services: [
...new Set(structuredSpans
.map((span) => span.service)
.filter((s) => s !== "N/A")),
],
rootSpans: structuredSpans.filter((span) => span.parentId === "ROOT")
.length,
timeRange: {
start: startTime || Math.floor(Date.now() / 1000) - timeRange,
end: endTime || Math.floor(Date.now() / 1000),
},
traceUrl: traceUrl,
},
spans: structuredSpans,
};
// Output the structured data as JSON for easy parsing by an agent
console.log(JSON.stringify(outputData, null, 2));
}
}
catch (error) {
console.error("Error retrieving trace spans:", error);
// Provide helpful suggestions based on the error
if (error.statusCode === 401) {
console.error("\nSuggestions:");
console.error("1. Check that your Honeycomb API key is valid and has the necessary permissions");
console.error("2. Make sure you've set the HONEYCOMB_API_KEY environment variable");
}
else if (error.statusCode === 404) {
console.error("\nSuggestions:");
console.error("1. Verify that the dataset exists");
console.error("2. Check that the trace ID is correct");
}
process.exit(1);
}
}
// Import fetch for Node.js environments
import fetch, { Headers } from "node-fetch";
// Helper function to make API requests
async function request(apiKey, apiEndpoint, path, options = {}) {
const url = `${apiEndpoint}${path}`;
const headers = new Headers({
"X-Honeycomb-Team": apiKey,
"Content-Type": "application/json",
"User-Agent": "honeycomb-trace-spans/1.0.0",
...options.headers,
});
const response = await fetch(url, {
...options,
headers,
});
if (!response.ok) {
const errorText = await response.text();
let errorMessage = `Honeycomb API error: ${response.statusText}`;
try {
const errorJson = JSON.parse(errorText);
if (errorJson.error) {
errorMessage = `Honeycomb API error: ${errorJson.error}`;
}
}
catch (e) {
// If we can't parse the error as JSON, use the raw text
if (errorText) {
errorMessage = `Honeycomb API error: ${errorText}`;
}
}
const error = new Error(errorMessage);
error.statusCode = response.status;
throw error;
}
return response.json();
}
// Run the script
main().catch(console.error);