Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ export class FileDiscoveryService {

/**
* A file is included when it matches an include pattern and no exclude pattern.
* Patterns are matched against the base name (a direct file path is reduced to it).
* Exclude patterns are matched against the base name; include patterns against `filename`
* as given (directory scans pass the base name, direct file paths pass the full path).
*/
private isIncluded(filename: string, patterns: string[], excludePatterns: string[]): boolean {
const baseName = filename.split(/[/\\]/).pop() ?? filename;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ describe('LogFileProcessor with *arr multi-line errors', () => {
expect(entries[0]?.stackTrace).toBeUndefined();
});

it('does not attach or emit an unparseable non-continuation line between entries', () => {
// 8-digit fraction does not match the NLog format (known limitation) so it is a continuation
// of the pending entry; the surrounding entries must still be emitted intact.
it('keeps an unparseable line (8-digit fraction) as a continuation of the pending entry, not as its own entry', () => {
// An 8-digit fraction does not match the NLog format (known limitation), so the line is
// folded into the pending entry's stack trace; the surrounding entries are emitted intact.
const entries = feed([
'2026-10-10 12:00:05.4|Info|A|one',
'2026-10-10 12:00:05.12345678|Info|B|weird',
Expand Down
26 changes: 26 additions & 0 deletions packages/provider-arr/src/arr.parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,32 @@ describe('Arr Parser', () => {
expect(result.message).toBe('bad state happened');
});

it.each([
'System.Exception',
'Npgsql.PostgresException',
'SQLite.SQLiteException',
'Polly.Timeout.TimeoutRejectedException',
'FluentValidation.ValidationException',
])('recognises %s outside the old allowlist', (type) => {
const result = finalizeArrLogEntry(entry, [
`[v4.0.10.2544] ${type}: it broke`,
' at Foo()',
]);

expect(result.exception).toBe(type);
expect(result.message).toBe('Boom - it broke');
expect(isArrLogContinuation(`${type}: it broke`)).toBe(true);
});

it.each([
'Exception: it broke',
'Request failed. Exception: it broke',
'Something.Exceptional: it broke',
'Some.Type Exception: it broke',
])('does not treat %j as an exception line', (line) => {
expect(finalizeArrLogEntry(entry, [line])).toBe(entry);
});

it('returns the entry unchanged when there is no exception line', () => {
expect(finalizeArrLogEntry(entry, [' at Foo.Bar()'])).toBe(entry);
});
Expand Down
20 changes: 11 additions & 9 deletions packages/provider-arr/src/arr.parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,13 @@ export function parseArrLogLine(line: string): ParsedLogEntry | null {
};
}

/**
* A .NET exception line: a namespace-qualified type name ending in `Exception`, then `:`.
* Requires at least one dot and no whitespace in the type, so it matches `System.Exception`,
* `Npgsql.PostgresException` or `Polly.Timeout.TimeoutRejectedException` but not ordinary prose.
*/
const EXCEPTION_LINE_REGEX = /^((?:\w+\.)+\w*Exception):\s*(.*)$/;

/**
* Check if a line is a continuation of a previous log entry
*
Expand All @@ -259,7 +266,7 @@ export function isArrLogContinuation(line: string): boolean {
if (/^\s{2,}at\s+/.test(line)) return true;

// .NET exception types
if (/^(System|Microsoft|NLog|NzbDrone)\.\w+(\.\w+)*Exception:/i.test(trimmed)) return true;
if (EXCEPTION_LINE_REGEX.test(trimmed)) return true;

// Inner exception marker
if (/^--->\s+/.test(trimmed)) return true;
Expand All @@ -279,13 +286,8 @@ export function isArrLogContinuation(line: string): boolean {
}

/**
* Parse a log line with context for multi-line entry support.
*
* Follows the `MediaServerProvider.parseLogLineWithContext` contract: `entry` is the
* entry parsed from THIS line (not the previous one), continuation lines are flagged with
* `isContinuation` so the caller can buffer them, and `previousComplete` tells the caller
* the previously pending entry is finished. Attaching continuation lines is the caller's
* job; use `finalizeArrLogEntry` to enrich the assembled entry with exception details.
* Parse a log line with context for multi-line entry support (`entry` is the entry for
* THIS line). Use `finalizeArrLogEntry` to enrich the assembled entry with exception details.
*/
export function parseArrLogLineWithContext(line: string, context: LogParseContext): LogParseResult {
// Blank lines carry no information. NLog emits one between the header and the
Expand Down Expand Up @@ -322,7 +324,7 @@ export function finalizeArrLogEntry(
const match = first
?.trim()
.replace(/^\[v[\d.]+\]\s*/, '')
.match(/^((?:System|Microsoft|NLog|NzbDrone)\.\w+(?:\.\w+)*Exception):\s*(.*)$/i);
.match(EXCEPTION_LINE_REGEX);
if (!match) return entry;

const [, exception, exceptionMessage] = match;
Expand Down