Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion public/app.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/app.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions public/mix-manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"/app.js": "/app.js?id=74d3e481ad3e8fa14f1daaa0aa46e109",
"/app.css": "/app.css?id=5593a0331dd40729ff41e32a6035d872",
"/app.js": "/app.js?id=8cfcfcd4a67b98d6d1cca994164fbb8e",
"/app.css": "/app.css?id=ffb86c9991a1ac32492c4abb96cd75f4",
"/img/log-viewer-128.png": "/img/log-viewer-128.png?id=d576c6d2e16074d3f064e60fe4f35166",
"/img/log-viewer-32.png": "/img/log-viewer-32.png?id=f8ec67d10f996aa8baf00df3b61eea6d",
"/img/log-viewer-64.png": "/img/log-viewer-64.png?id=8902d596fc883ca9eb8105bb683568c6"
Expand Down
104 changes: 100 additions & 4 deletions resources/js/components/BaseLogTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,45 @@
<mail-text-preview :mail="log.extra.mail_preview" />
</tab-content>

<tab-content v-if="hasLaravelStackTrace(log)" tab-value="stack_trace">
<div class="p-4 lg:p-8">
<!-- Exception Header -->
<div v-if="getStackTraceData(log).header" class="mb-6 pb-4 border-b border-gray-200 dark:border-gray-600">
<div class="text-red-600 dark:text-red-400 font-semibold text-lg mb-2">
{{ getStackTraceData(log).header.type }}
</div>
<div class="text-gray-800 dark:text-gray-200 text-base mb-2">
{{ getStackTraceData(log).header.message }}
</div>
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono">
in {{ getStackTraceData(log).header.file }}:{{ getStackTraceData(log).header.line }}
</div>
</div>

<!-- Stack Trace Frames -->
<div class="space-y-2">
<div v-for="(frame, frameIndex) in getStackTraceData(log).frames" :key="frameIndex"
class="mb-2 border-b border-gray-100 dark:border-gray-700 pb-2 last:border-b-0">
<div class="flex items-start gap-3">
<div class="text-xs text-gray-500 dark:text-gray-400 font-mono w-8 flex-shrink-0 pt-1">
#{{ frame.number }}
</div>
<div class="flex-1 min-w-0">
<div v-if="frame.file" class="text-sm mb-1">
<span class="font-mono text-blue-600 dark:text-blue-400 break-all">{{ frame.file }}</span>
<span class="text-gray-500 dark:text-gray-400">:</span>
<span class="font-mono text-orange-600 dark:text-orange-400">{{ frame.line }}</span>
</div>
<div class="text-sm text-gray-800 dark:text-gray-200 font-mono break-all">
{{ frame.call }}
</div>
</div>
</div>
</div>
</div>
</div>
</tab-content>

<tab-content tab-value="raw">
<pre class="log-stack" v-html="highlightSearchResult(log.full_text, searchStore.query)"></pre>
<template v-if="hasContext(log)">
Expand Down Expand Up @@ -195,10 +234,16 @@ const getExtraTabsForLog = (log) => {
}

const getTabsForLog = (log) => {
return [
...getExtraTabsForLog(log),
{ name: 'Raw', value: 'raw' },
].filter(Boolean);
const tabs = [...getExtraTabsForLog(log)];

tabs.push({ name: 'Raw', value: 'raw' });

// Add Stack Trace tab for Laravel logs with stack traces
if (hasLaravelStackTrace(log)) {
tabs.push({ name: 'Stack Trace', value: 'stack_trace' });
}

return tabs.filter(Boolean);
}

const prepareContextForOutput = (context) => {
Expand All @@ -211,6 +256,57 @@ const prepareContextForOutput = (context) => {
}, 2);
}

const hasLaravelStackTrace = (log) => {
const exception = Array.isArray(log.context)
? log.context.find(item => item.exception)?.exception
: log.context.exception;
return exception && typeof exception === 'string' && exception.includes('[stacktrace]');
}

const getStackTraceData = (log) => {
const exception = Array.isArray(log.context)
? log.context.find(item => item.exception)?.exception
: log.context.exception;

if (!exception || typeof exception !== 'string') {
return { header: null, frames: [] };
}

// Parse exception header
const headerMatch = exception.match(/^\[object\]\s*\(([^(]+)\(code:\s*\d+\):\s*(.+?)\s+at\s+(.+?):(\d+)\)/);
const header = headerMatch ? {
type: headerMatch[1].trim(),
message: headerMatch[2].trim(),
file: headerMatch[3].trim(),
line: parseInt(headerMatch[4])
} : null;

// Parse stack trace frames
const stacktraceMatch = exception.match(/\[stacktrace\]([\s\S]*?)(?:\n\n|\n$|$)/);
const frames = [];
if (stacktraceMatch) {
const frameRegex = /#(\d+)\s+(.+?)(?:\n|$)/g;
let match;
while ((match = frameRegex.exec(stacktraceMatch[1])) !== null) {
const frameLine = match[2].trim();
const fileMatch = frameLine.match(/^(.+?)\((\d+)\):\s*(.+)$/);
frames.push(fileMatch ? {
number: parseInt(match[1]),
file: fileMatch[1],
line: parseInt(fileMatch[2]),
call: fileMatch[3]
} : {
number: parseInt(match[1]),
file: '',
line: 0,
call: frameLine
});
}
}

return { header, frames };
}

const tableColumns = computed(() => {
// the extra two columns are for the expand/collapse and log index columns
return logViewerStore.columns.length + 2;
Expand Down