Files
Mihai Popescu c8686b5f1c feat: tri-6738 Create aggregated logs page (#2862)
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>
2026-01-13 11:21:13 +00:00

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;
}
}