c8686b5f1c
Closes #<issue> ## ✅ Checklist - [x] I have followed every step in the [contributing guide](https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md) - [x] The PR title follows the convention. - [x] I ran and tested the code works --- ## Testing - Verified log detail view displays correctly with message, metadata, and attributes - Tested search highlighting functionality in log messages (escapes special regex characters) - Confirmed tabs (Details/Run) switch properly with keyboard shortcuts (d/r) - Verified run information loads via async fetcher in Run tab - Tested close button and Escape key for dismissing the panel - Verified log details display correct information: level badges, kind badges, timestamps, trace IDs, span IDs - Confirmed links to parent spans and run pages work correctly - Tested with various log levels (ERROR, WARN, INFO, DEBUG, TRACE) and kinds (SPAN, SPAN_EVENT, LOG_*) - Verified admin-only fields display correctly when user has admin access - Tested data loading states and error states (log not found, run not found) --- ## Changelog Created new Logs page. The information shown is gathered from the spans from each run. The feature supports all run filters with two new filters for level and logs text search. --- ## Screenshots <img width="2059" height="1196" alt="Logs page preview" src="https://github.com/user-attachments/assets/70b667b4-98cc-4728-855a-2766dd5c1aa5" /> 💯 --------- Co-authored-by: James Ritchie <james@trigger.dev>
156 lines
4.3 KiB
TypeScript
156 lines
4.3 KiB
TypeScript
import { createElement, Fragment, type ReactNode } from "react";
|
|
import { z } from "zod";
|
|
|
|
export const LogLevelSchema = z.enum(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "CANCELLED"]);
|
|
export type LogLevel = z.infer<typeof LogLevelSchema>;
|
|
|
|
export const validLogLevels: LogLevel[] = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "CANCELLED"];
|
|
|
|
// Default styles for search highlighting
|
|
const DEFAULT_HIGHLIGHT_STYLES: React.CSSProperties = {
|
|
backgroundColor: "#facc15", // yellow-400
|
|
color: "#000000",
|
|
fontWeight: "500",
|
|
borderRadius: "0.25rem",
|
|
padding: "0 0.125rem",
|
|
} as const;
|
|
|
|
/**
|
|
* Highlights all occurrences of a search term in text with consistent styling.
|
|
* Case-insensitive search with regex special character escaping.
|
|
*
|
|
* @param text - The text to search within
|
|
* @param searchTerm - The term to highlight (optional)
|
|
* @param style - Optional custom inline styles for highlights
|
|
* @returns React nodes with highlighted matches, or the original text if no matches
|
|
*/
|
|
export function highlightSearchText(
|
|
text: string,
|
|
searchTerm?: string,
|
|
style: React.CSSProperties = DEFAULT_HIGHLIGHT_STYLES
|
|
): ReactNode {
|
|
if (!searchTerm || searchTerm.trim() === "") {
|
|
return text;
|
|
}
|
|
|
|
// Defense in depth: limit search term length to prevent ReDoS and performance issues
|
|
const MAX_SEARCH_LENGTH = 500;
|
|
if (searchTerm.length > MAX_SEARCH_LENGTH) {
|
|
return text;
|
|
}
|
|
|
|
// Escape special regex characters in search term
|
|
const escapedSearch = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
const regex = new RegExp(escapedSearch, "gi");
|
|
|
|
const parts: ReactNode[] = [];
|
|
let lastIndex = 0;
|
|
let match;
|
|
let matchCount = 0;
|
|
|
|
while ((match = regex.exec(text)) !== null) {
|
|
// Add text before match
|
|
if (match.index > lastIndex) {
|
|
parts.push(text.substring(lastIndex, match.index));
|
|
}
|
|
// Add highlighted match
|
|
parts.push(
|
|
createElement("span", { key: `match-${matchCount}`, style }, match[0])
|
|
);
|
|
lastIndex = regex.lastIndex;
|
|
matchCount++;
|
|
}
|
|
|
|
// Add remaining text
|
|
if (lastIndex < text.length) {
|
|
parts.push(text.substring(lastIndex));
|
|
}
|
|
|
|
return parts.length > 0 ? parts : text;
|
|
}
|
|
|
|
// Convert ClickHouse kind to display level
|
|
export function kindToLevel(kind: string, status: string): LogLevel {
|
|
if (status === "CANCELLED") {
|
|
return "CANCELLED";
|
|
}
|
|
|
|
// ERROR can come from either kind or status
|
|
if (kind === "LOG_ERROR" || status === "ERROR") {
|
|
return "ERROR";
|
|
}
|
|
|
|
switch (kind) {
|
|
case "DEBUG_EVENT":
|
|
case "LOG_DEBUG":
|
|
return "DEBUG";
|
|
case "LOG_INFO":
|
|
return "INFO";
|
|
case "LOG_WARN":
|
|
return "WARN";
|
|
case "LOG_LOG":
|
|
return "INFO"; // Changed from "LOG"
|
|
case "SPAN":
|
|
case "ANCESTOR_OVERRIDE":
|
|
case "SPAN_EVENT":
|
|
default:
|
|
return "TRACE";
|
|
}
|
|
}
|
|
|
|
// Level badge color styles
|
|
export function getLevelColor(level: LogLevel): string {
|
|
switch (level) {
|
|
case "ERROR":
|
|
return "text-error bg-error/10 border-error/20";
|
|
case "WARN":
|
|
return "text-warning bg-warning/10 border-warning/20";
|
|
case "DEBUG":
|
|
return "text-charcoal-400 bg-charcoal-700 border-charcoal-600";
|
|
case "INFO":
|
|
return "text-blue-400 bg-blue-500/10 border-blue-500/20";
|
|
case "TRACE":
|
|
return "text-charcoal-500 bg-charcoal-800 border-charcoal-700";
|
|
case "CANCELLED":
|
|
return "text-charcoal-400 bg-charcoal-700 border-charcoal-600";
|
|
default:
|
|
return "text-text-dimmed bg-charcoal-750 border-charcoal-700";
|
|
}
|
|
}
|
|
|
|
// Event kind badge color styles
|
|
export function getKindColor(kind: string): string {
|
|
if (kind === "SPAN") {
|
|
return "text-purple-400 bg-purple-500/10 border-purple-500/20";
|
|
}
|
|
if (kind === "SPAN_EVENT") {
|
|
return "text-amber-400 bg-amber-500/10 border-amber-500/20";
|
|
}
|
|
if (kind.startsWith("LOG_")) {
|
|
return "text-blue-400 bg-blue-500/10 border-blue-500/20";
|
|
}
|
|
return "text-charcoal-400 bg-charcoal-700 border-charcoal-600";
|
|
}
|
|
|
|
// Get human readable kind label
|
|
export function getKindLabel(kind: string): string {
|
|
switch (kind) {
|
|
case "SPAN":
|
|
return "Span";
|
|
case "SPAN_EVENT":
|
|
return "Event";
|
|
case "LOG_DEBUG":
|
|
case "LOG_INFO":
|
|
case "LOG_WARN":
|
|
case "LOG_ERROR":
|
|
case "LOG_LOG":
|
|
return "Log";
|
|
case "DEBUG_EVENT":
|
|
return "Debug";
|
|
case "ANCESTOR_OVERRIDE":
|
|
return "Override";
|
|
default:
|
|
return kind;
|
|
}
|
|
}
|