Skip to content

Conversation

@elibosley
Copy link
Member

@elibosley elibosley commented Mar 13, 2025

Summary by CodeRabbit

  • New Features
    • Introduced a comprehensive Log Viewer accessible from the web interface and Unraid management, allowing users to easily view, refresh, and download log files.
    • Enabled real-time log updates with auto-scroll functionality for seamless monitoring.
    • Enhanced log display with syntax highlighting and detailed file metadata for improved readability.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 13, 2025

Walkthrough

This pull request introduces a new log file management capability across the backend and frontend systems. The API now includes a new pub/sub channel and enhanced GraphQL schema for log queries and subscriptions. A fresh NestJS Logs module—with corresponding resolver and service classes—handles log file operations and real-time file watching. On the frontend, new Vue components integrate GraphQL queries, subscriptions, and a dedicated Unraid log viewer page. Additional configuration updates, dependency additions, and tests support the expanded functionality.

Changes

File(s) Summary
api/src/core/pubsub.ts, api/src/store/modules/paths.ts Added LOG_FILE channel to pub/sub and a new 'unraid-log-base' path entry.
api/src/graphql/schema/types/logs/logs.graphql Introduced new GraphQL types, queries, and subscriptions for log file data.
api/src/unraid-api/graph/resolvers/logs/ (all files) and api/src/unraid-api/graph/resolvers/resolvers.module.ts Added a new Logs module including LogsModule, LogsResolver (with queries and subscriptions), LogsService for file operations, and corresponding tests.
web/components/Logs/ (all files), web/composables/gql/ (all files), web/pages/index.vue, web/package.json Introduced new Vue components (LogViewer and SingleLogViewer), GraphQL queries/subscription definitions, and a dependency on highlight.js for log viewing functionality.
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/LogViewer.page, web/nuxt.config.ts Added Unraid log viewer page and registered custom element for it in Nuxt config.
api/src/__test__/store/modules/paths.test.ts Updated snapshot to include the new 'unraid-log-base' path entry.

Sequence Diagram(s)

sequenceDiagram
    participant C as Client
    participant GQL as GraphQL Endpoint
    participant R as LogsResolver
    participant S as LogsService
    C->>GQL: Send logFiles query
    GQL->>R: Forward query to resolver
    R->>S: Call listLogFiles()
    S-->>R: Return list of log files
    R-->>GQL: Return logFiles response
    GQL-->>C: Deliver data
Loading
sequenceDiagram
    participant C as Client
    participant GQL as GraphQL Endpoint
    participant R as LogsResolver
    participant S as LogsService
    participant FS as File System Watcher
    C->>GQL: Subscribe to logFile updates
    GQL->>R: Forward subscription
    R->>S: Start watching log file
    S->>FS: Initiate file watcher (chokidar)
    FS-->>S: File update event
    S->>R: Publish updated log content via pub/sub
    R-->>GQL: Push update to subscriber
    GQL-->>C: Deliver live update
Loading

Suggested reviewers

  • pujitm
  • mdatelle

Poem

In lines of code a log unfolds,
A secret stream in data told.
Through watchers and queries, magic flows,
Real-time updates in graceful shows.
Logs now sing in bytes and art,
A CodeRabbit masterpiece from the start!
🚀✨


🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@elibosley elibosley requested review from mdatelle and pujitm March 13, 2025 18:55
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
api/src/unraid-api/graph/resolvers/logs/logs.module.ts (1)

1-9: Module structure looks good, but please fix formatting issues.

The NestJS module structure follows conventions with correct providers and exports. However, the static analysis is flagging several formatting issues:

  1. Use absolute paths for imports
  2. Add a blank line after imports
  3. Fix indentation with proper spaces
  4. Add a newline at the end of the file
-import { Module } from '@nestjs/common';
-import { LogsResolver } from './logs.resolver.js';
-import { LogsService } from './logs.service.js';
+import { Module } from '@nestjs/common';
+
+import { LogsResolver } from 'src/unraid-api/graph/resolvers/logs/logs.resolver.js';
+import { LogsService } from 'src/unraid-api/graph/resolvers/logs/logs.service.js';

 @Module({
-  providers: [LogsResolver, LogsService],
-  exports: [LogsService],
+  providers: [LogsResolver, LogsService],
+  exports: [LogsService],
 })
-export class LogsModule {} 
+export class LogsModule {}
🧰 Tools
🪛 GitHub Check: Test API

[failure] 2-2:
Insert


[failure] 2-2:
import statements should have an absolute path


[failure] 3-3:
import statements should have an absolute path


[failure] 6-6:
Insert ··


[failure] 7-7:
Insert ··


[failure] 9-9:
Replace · with

🪛 GitHub Check: Build API

[failure] 2-2:
Insert


[failure] 2-2:
import statements should have an absolute path


[failure] 3-3:
import statements should have an absolute path


[failure] 6-6:
Insert ··


[failure] 7-7:
Insert ··


[failure] 9-9:
Replace · with

api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts (1)

15-33: Optional lines argument usage.
You might want to explicitly default the lines argument to a sensible value in the GraphQL schema or handle edge cases (e.g., negative values) on the service layer.

api/src/graphql/schema/types/logs/logs.graphql (1)

15-66: Consider renaming the subscription field for clarity.
Using a distinct field name like logFileUpdated may help differentiate it from the logFile query.

Here’s an example diff:

-  logFile(path: String!): LogFileContent!
+  logFileUpdated(path: String!): LogFileContent!
api/src/unraid-api/graph/resolvers/logs/logs.service.ts (2)

71-91: Potential performance concern with large files.
Reading and counting lines for very large log files may be slow. Caching or chunked reads could improve performance.


112-185: Watch for error scenarios in file watching.
The code correctly handles file truncation, but if a file is moved or locked while reading, consider additional logging or retry strategies.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 4409be1 and 7173a8b.

⛔ Files ignored due to path filters (2)
  • api/src/graphql/generated/api/operations.ts is excluded by !**/generated/**
  • api/src/graphql/generated/api/types.ts is excluded by !**/generated/**
📒 Files selected for processing (8)
  • api/src/core/pubsub.ts (1 hunks)
  • api/src/graphql/schema/types/logs/logs.graphql (1 hunks)
  • api/src/store/modules/paths.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/logs/logs.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/logs/logs.resolver.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/logs/logs.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts (2 hunks)
🧰 Additional context used
🪛 GitHub Check: Test API
api/src/unraid-api/graph/resolvers/logs/logs.module.ts

[failure] 2-2:
Insert


[failure] 2-2:
import statements should have an absolute path


[failure] 3-3:
import statements should have an absolute path


[failure] 6-6:
Insert ··


[failure] 7-7:
Insert ··


[failure] 9-9:
Replace · with

api/src/unraid-api/graph/resolvers/logs/logs.resolver.spec.ts

[failure] 2-2:
Insert


[failure] 2-2:
import statements should have an absolute path


[failure] 5-5:
Insert ··


[failure] 7-7:
Insert ··

🪛 GitHub Check: Build API
api/src/unraid-api/graph/resolvers/logs/logs.module.ts

[failure] 2-2:
Insert


[failure] 2-2:
import statements should have an absolute path


[failure] 3-3:
import statements should have an absolute path


[failure] 6-6:
Insert ··


[failure] 7-7:
Insert ··


[failure] 9-9:
Replace · with

api/src/unraid-api/graph/resolvers/logs/logs.resolver.spec.ts

[failure] 2-2:
Insert


[failure] 2-2:
import statements should have an absolute path


[failure] 5-5:
Insert ··


[failure] 7-7:
Insert ··

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Build Web App
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (9)
api/src/core/pubsub.ts (1)

21-21: New LOG_FILE channel looks good!

This addition aligns with the PR's purpose of adding log file management features.

api/src/store/modules/paths.ts (1)

63-63: Path addition looks good!

Adding the 'unraid-log-base' path pointing to the standard Linux log location is appropriate for the log file management feature.

api/src/unraid-api/graph/resolvers/resolvers.module.ts (2)

22-23: Imports look good!
These additions properly integrate the new logging components.


46-47: Confirm whether to export logs functionality.
If LogsResolver and LogsService are required by other modules, consider exporting them here for broader availability. Otherwise, this is fine.

api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts (2)

1-13: Good overall structure for the new resolver.
The constructor injection of LogsService appears clean and maintainable. Make sure there is test coverage for each method in this resolver.


35-61: Validate subscription teardown logic.
Overriding the return method effectively halts file monitoring, but please double-check concurrency scenarios if multiple subscriptions exist for the same file.

api/src/graphql/schema/types/logs/logs.graphql (1)

1-13: Clear documentation for queries.
The docstring for logFile clarifies how many lines default to 100 if none is provided, which is consistent with the implementation.

api/src/unraid-api/graph/resolvers/logs/logs.service.ts (2)

28-35: Ensure secure path handling.
Using basename helps, but consider further checks to guard against path traversal or symbolic links that might lead outside the intended directory.


40-64: Efficient implementation for listing logs.
The approach is correct. Handling errors and returning an empty list is reasonable.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts (1)

23-31: Consider validating the 'lines' parameter.

While the implementation is clean, consider adding validation for the 'lines' parameter to ensure it's a positive number.

-    async logFile(@Args('path') path: string, @Args('lines') lines?: number) {
+    async logFile(@Args('path') path: string, @Args('lines') lines?: number) {
+        if (lines !== undefined && lines <= 0) {
+            throw new Error('The lines parameter must be a positive number');
+        }
         return this.logsService.getLogFileContent(path, lines);
     }
api/src/unraid-api/graph/resolvers/logs/logs.service.ts (1)

207-232: Consider more efficient line counting for large files.

The current implementation reads the entire file line by line, which could be inefficient for very large log files.

Consider using a more efficient approach for large files, such as reading in chunks and counting newlines.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 7173a8b and f760c2e.

📒 Files selected for processing (5)
  • api/src/unraid-api/graph/resolvers/logs/logs.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/logs/logs.resolver.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/logs/logs.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • api/src/unraid-api/graph/resolvers/logs/logs.module.ts
  • api/src/unraid-api/graph/resolvers/logs/logs.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Build Web App
  • GitHub Check: Test API
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (13)
api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts (4)

5-7: Good import organization.

The imports are well-organized, with a clear separation between external libraries and internal modules.


9-11: Clean resolver implementation with dependency injection.

The resolver is properly decorated and uses constructor injection for the LogsService.


13-21: Permission protected query implementation looks good.

The logFiles query is properly protected with permission checks and the implementation is straightforward.


33-61: Well-implemented subscription with proper resource cleanup.

The subscription implementation is excellent. You've appropriately:

  1. Started watching the file
  2. Created the async iterator
  3. Overridden the return method to clean up resources
  4. Ensured proper error handling

This approach properly cleans up resources when clients disconnect from the subscription.

api/src/unraid-api/graph/resolvers/logs/logs.service.ts (9)

1-11: Good import organization and dependency management.

The imports are well-organized, and you've properly imported the necessary modules for file operations.


12-23: Well-defined interfaces for log file data.

The interfaces for LogFile and LogFileContent are clear and comprehensive.


25-36: Good service initialization and configuration access.

The injectable service is well-structured with proper logger initialization and a clean getter for the log base path.


38-65: Good error handling in the listLogFiles method.

The implementation properly handles errors by logging them and gracefully returning an empty array instead of crashing.


67-94: Path validation is implemented correctly for security.

Good job normalizing the path to prevent directory traversal attacks with join(this.logBasePath, basename(path)).


96-109: Clean subscription channel management.

The method correctly normalizes paths and initiates file watching only when needed.


111-191: Robust file watching implementation with good error handling.

The file watching implementation handles both file growth and truncation scenarios well. The error handling is comprehensive with detailed logging.


193-205: Proper resource cleanup in stopWatchingLogFile.

The method correctly closes watchers and removes them from the tracking map.


234-268: Reading last N lines may be optimized.

Currently, every line is read to compute the final N lines. For large files, consider a reverse seek approach.

A more efficient approach would be to read from the end of the file in chunks and count backwards until the desired number of lines is reached.

private async readLastLines(filePath: string, lineCount: number): Promise<string> {
-    const totalLines = await this.countFileLines(filePath);
-    const linesToSkip = Math.max(0, totalLines - lineCount);
-
-    return new Promise((resolve, reject) => {
-        let currentLine = 0;
-        let content = '';
-
-        const stream = createReadStream(filePath);
-        const rl = createInterface({
-            input: stream,
-            crlfDelay: Infinity,
-        });
-
-        rl.on('line', (line) => {
-            currentLine++;
-            if (currentLine > linesToSkip) {
-                content += line + '\n';
-            }
-        });
-
-        rl.on('close', () => {
-            resolve(content);
-        });
-
-        rl.on('error', (err) => {
-            reject(err);
-        });
-    });
+    return new Promise(async (resolve, reject) => {
+        try {
+            const { size } = await stat(filePath);
+            const fileBuffer = Buffer.alloc(size);
+            
+            // Open file for reading
+            const fileHandle = await open(filePath, 'r');
+            
+            // Read the entire file
+            await fileHandle.read(fileBuffer, 0, size, 0);
+            await fileHandle.close();
+            
+            // Convert buffer to string and split by newlines
+            const lines = fileBuffer.toString().split('\n');
+            
+            // Get last N lines
+            const lastLines = lines.slice(-lineCount).join('\n');
+            resolve(lastLines + (lastLines.endsWith('\n') ? '' : '\n'));
+        } catch (err) {
+            reject(err);
+        }
+    });
}

Note: For extremely large files, you might want to implement a more sophisticated solution that reads chunks from the end of the file.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (12)
web/components/Logs/LogViewer.ce.vue (2)

30-38: Consider extracting utility function

The formatFileSize function is well-implemented but consider moving it to a shared utility file if it might be used elsewhere in the application.

- // Format file size for display
- const formatFileSize = (bytes: number): string => {
-   if (bytes === 0) return '0 Bytes';
-   
-   const k = 1024;
-   const sizes = ['Bytes', 'KB', 'MB', 'GB'];
-   const i = Math.floor(Math.log(bytes) / Math.log(k));
-   
-   return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
- };

Consider creating a utility file:

// utils/format.ts
export const formatFileSize = (bytes: number): string => {
  if (bytes === 0) return '0 Bytes';
  
  const k = 1024;
  const sizes = ['Bytes', 'KB', 'MB', 'GB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};

Then import it:

import { formatFileSize } from '~/utils/format';

111-113: Empty style section

The style section contains only a comment about importing UI globals but doesn't actually import anything. If you're not using any custom styles, consider removing this section.

- <style lang="postcss">
- /* Import unraid-ui globals first */
- </style>
api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts (1)

30-30: Potential extraneous whitespace or character detected.
Static analysis reported “Delete ·” at line 30. Please verify and remove any hidden or trailing characters.

-        @Args('path') path: string, 
+        @Args('path') path: string,
🧰 Tools
🪛 GitHub Check: Build API

[failure] 30-30:
Delete ·

🪛 GitHub Check: Test API

[failure] 30-30:
Delete ·

api/src/graphql/schema/types/logs/logs.graphql (2)

24-47: Add an optional field for log file type or location metadata.
Currently, LogFile contains basic file info. If multi-directory or different log categories are needed in the future, you may consider extending this type with optional metadata or tags to enhance sorting and filtering.


49-72: Check line count defaults for large files.
The LogFileContent fields work fine for typical logs, but be mindful that returning large logs entirely might impact performance. Consider pagination or chunking for extremely large files, if necessary.

api/src/unraid-api/graph/resolvers/logs/logs.service.ts (7)

68-108: Handle out-of-range values for lines or startLine.
getLogFileContent() should gracefully handle negative or zero line counts, as well as startLine values exceeding the file size. This can prevent unexpected slices or empty content results.

🧰 Tools
🪛 GitHub Check: Build API

[failure] 75-75:
Delete ·


[failure] 76-76:
Delete ·


[failure] 87-87:
Delete ············


[failure] 100-100:
Insert ,

🪛 GitHub Check: Test API

[failure] 75-75:
Delete ·


[failure] 76-76:
Delete ·


[failure] 87-87:
Delete ············


[failure] 100-100:
Insert ,


125-205: Prevent potential memory overflow when files grow rapidly.
startWatchingLogFile() reads updates into a string. For extremely large bursts of new data, consider streaming chunk by chunk or limiting buffer sizes to reduce memory usage.


207-219: Add usage metrics instrumentation (optional).
stopWatchingLogFile() is a good housekeeping method. For logs with high churn, you might also track usage metrics or add scheduling logic to close watchers automatically after a certain period of inactivity.


221-246: Potential performance bottleneck for massive files.
countFileLines() iterates over all lines, which can be expensive for huge logs. If line counts are frequently requested, consider indexing or caching the line count if feasible.


248-282: Leverage a more efficient approach for reading last N lines.
Reading the entire file to retrieve the last lines can be slow for large logs. Consider a reverse seek or other specialized approach to read from the end without scanning the entire file.


284-330: Optimize partial reads from large files.
readLinesFromPosition() works but may still require linear parsing up to startLine. If logs are extremely large, you could implement skip logic or approximate offsets.

🧰 Tools
🪛 GitHub Check: Build API

[failure] 291-291:
Delete ·


[failure] 292-292:
Delete ·


[failure] 308-308:
Delete ················

🪛 GitHub Check: Test API

[failure] 291-291:
Delete ·


[failure] 292-292:
Delete ·


[failure] 308-308:
Delete ················


75-76: Remove extra whitespace to fix pipeline lint failures.
The static analysis hints indicate that there are extraneous whitespace characters on these lines. Removing them ensures formatting compliance and avoids build failures.

-··
-        // or other extraneous spaces
+// remove trailing or extra indentation

Also applies to: 87-87, 100-100, 291-292, 308-308

🧰 Tools
🪛 GitHub Check: Build API

[failure] 75-75:
Delete ·


[failure] 76-76:
Delete ·

🪛 GitHub Check: Test API

[failure] 75-75:
Delete ·


[failure] 76-76:
Delete ·

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between f760c2e and d79717a.

📒 Files selected for processing (10)
  • api/src/graphql/schema/types/logs/logs.graphql (1 hunks)
  • api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/logs/logs.service.ts (1 hunks)
  • web/components/Logs/LogViewer.ce.vue (1 hunks)
  • web/components/Logs/SingleLogViewer.vue (1 hunks)
  • web/components/Logs/log.query.ts (1 hunks)
  • web/components/Logs/log.subscription.ts (1 hunks)
  • web/composables/gql/gql.ts (3 hunks)
  • web/composables/gql/graphql.ts (9 hunks)
  • web/pages/index.vue (2 hunks)
🧰 Additional context used
🪛 GitHub Check: Build API
api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts

[failure] 30-30:
Delete ·

api/src/unraid-api/graph/resolvers/logs/logs.service.ts

[failure] 75-75:
Delete ·


[failure] 76-76:
Delete ·


[failure] 87-87:
Delete ············


[failure] 100-100:
Insert ,


[failure] 291-291:
Delete ·


[failure] 292-292:
Delete ·


[failure] 308-308:
Delete ················

🪛 GitHub Check: Test API
api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts

[failure] 30-30:
Delete ·

api/src/unraid-api/graph/resolvers/logs/logs.service.ts

[failure] 75-75:
Delete ·


[failure] 76-76:
Delete ·


[failure] 87-87:
Delete ············


[failure] 100-100:
Insert ,


[failure] 291-291:
Delete ·


[failure] 292-292:
Delete ·


[failure] 308-308:
Delete ················

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Build Web App
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (31)
web/components/Logs/log.subscription.ts (1)

1-11: Looks good: GraphQL subscription for log file updates

The GraphQL subscription is well-structured and includes all necessary fields for tracking log file updates in real-time. This will work well with the pubsub system for delivering log file changes to the frontend.

web/components/Logs/log.query.ts (2)

1-12: Well-structured query for retrieving log files

The GET_LOG_FILES query retrieves all necessary metadata for log files including name, path, size, and modification timestamp. This provides a complete picture for displaying log files in the UI.


14-23: Solid implementation of log file content query

The GET_LOG_FILE_CONTENT query is well-designed with optional parameters for pagination (lines and startLine). This provides flexibility for viewing different portions of large log files.

web/pages/index.vue (2)

11-11: Good component import

The LogViewerCe component is properly imported using the correct path.


187-191: Clean integration of Log Viewer component

The Log Viewer component is integrated consistently with the existing UI patterns, maintaining the same structure as other component sections with appropriate headers and styling.

web/components/Logs/LogViewer.ce.vue (3)

17-21: Well-designed component state

The reactive state for managing the log viewer is clearly defined with appropriate defaults. The auto-scroll default being enabled is a good choice for log viewing.


22-27: Effective GraphQL query integration

The component correctly uses the Apollo composable to fetch log files while exposing loading and error states for proper UI handling.


83-107: Excellent error handling and conditional rendering

The component handles all possible states appropriately: loading, error, empty log files list, and no selection. This creates a robust user experience with clear feedback.

api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts (4)

1-4: Clean and well-structured imports.
Everything looks standard and appropriate for a NestJS GraphQL Resolver.


13-21: Query method uses appropriate authorization checks.
The logFiles query method correctly delegates directory scanning to LogsService, and the use of @UsePermissions is consistent with security best practices.


23-35: Consider handling invalid arguments for logFile.
If path is invalid or points to a non-existent file, ensure LogsService.getLogFileContent gracefully handles errors. You may want to wrap and rethrow with user-friendly messages.

🧰 Tools
🪛 GitHub Check: Build API

[failure] 30-30:
Delete ·

🪛 GitHub Check: Test API

[failure] 30-30:
Delete ·


37-65: Subscription logic is robust with cleanup.
Overriding the asyncIterator.return method to stop file watching ensures resources are released on unsubscribe. Excellent approach to preventing resource leaks.

web/components/Logs/SingleLogViewer.vue (7)

1-8: Imports and setup look correct.
All necessary composables and UI elements are imported, including DOMPurify for safe HTML rendering. Great job ensuring sanitized log output.


13-17: Prop definitions are explicit and typed.
Defining logFilePath, lineCount, and autoScroll fosters clarity and helps prevent runtime errors.


19-28: State structure is comprehensive.
The reactive state object neatly tracks loading status, chunk data, and scrolling details to manage large log files effectively.


50-57: Smart scrolling method.
Using nextTick to scroll after DOM updates helps ensure the viewport is scrolled correctly. This is a good approach for log viewers.


71-92: Real-time subscription approach is well done.
Appending new log content on subscription updates keeps the viewer in sync. No concurrency concerns are apparent.


139-166: Infinite scroll implementation.
Loading older content chunks on scroll ensures usability for large logs. The toggling of canLoadMore and isLoadingMore is carefully handled.


168-181: Effective refresh strategy.
Resetting chunks and refetching the log ensures the view stays consistent if the user clicks Refresh or the file changes drastically.

web/composables/gql/graphql.ts (4)

600-624: Schema definitions for log files are clear.
Declaring LogFile and LogFileContent with fields for size, path, content, etc. is straightforward and well-documented.


1099-1108: Exposing new queries in the schema.
Adding logFile and logFiles to the Query type is a clean approach to retrieving logs.


1355-1359: Subscription fields integrate seamlessly.
The subscription for logFile complements Query.logFile, enabling real-time updates on file changes. Good job matching the arguments with the query.

Also applies to: 1390-1392


1799-1820: GraphQL operations for logs.
Queries and subscriptions (LogFilesQuery, LogFileContentQuery, LogFileSubscriptionSubscription) are well-typed with clear variables.

Also applies to: 1946-1948

api/src/graphql/schema/types/logs/logs.graphql (2)

1-14: Consider validating query parameters in the resolver.
While this schema is clean, ensure in your resolver logic that user inputs (e.g., negative values for lines or invalid startLine) are properly handled or validated to avoid errors or inconsistent data.


16-22: Evaluate error handling for subscription-based user inputs.
Review your subscription resolver to confirm that it gracefully handles invalid path inputs and other potential user errors, such as non-existent files or restricted access.

api/src/unraid-api/graph/resolvers/logs/logs.service.ts (3)

26-38: Service encapsulation looks solid.
Defining a dedicated LogsService is clear and maintains good separation of concerns.


39-66: Ensure consistent error handling strategy.
listLogFiles() returns an empty array on error, which is fine, but be sure the calling resolver or any higher-level logic is aware of this possibility and can surface it appropriately to clients.


110-123: Enforce path normalization.
getLogFileSubscriptionChannel normalizes the path before watching. Double-check that errors and security constraints are handled, e.g., if basename(path) differs from user input.

web/composables/gql/gql.ts (3)

16-19: Validate new queries in the generated types.
The new entries in Documents align well with your newly added operations. Verify that these references accurately match the schema definitions and no further transformations are needed.


40-43: Ensure consistent usage of the newly added documents.
The LogFiles, LogFileContent, and LogFileSubscription entries in documents appear correct. However, double-check that they’re used consistently in the consuming components.


79-90: Confirm the correctness of overload signatures.
The newly added function overloads for graphql properly map query strings to typed documents. Make sure the extended approach for large scale queries or future expansions is workable.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
web/components/Logs/LogViewer.ce.vue (1)

82-88: Minor suggestion for auto-detection approach.
Consider using a concise mapping to associate file extensions with languages for easier maintenance, rather than multiple conditionals.

-const autoDetectLanguage = (filePath: string): string => {
-  const fileName = filePath.split('/').pop() || '';
-  if (fileName.endsWith('.sh') || fileName.endsWith('.bash') || fileName.includes('syslog')) {
-    return 'bash';
-  } else if (fileName.endsWith('.conf') || ...)
-  ...
-  return 'plaintext';
-};
+const extensionToLanguageMap: Record<string, string> = {
+  '.sh': 'bash',
+  '.bash': 'bash',
+  '.conf': 'ini',
+  '.ini': 'ini',
+  '.cfg': 'ini',
+  // ...
+};
+
+function autoDetectLanguage(filePath: string): string {
+  const fileName = filePath.split('/').pop() || '';
+  for (const ext in extensionToLanguageMap) {
+    if (fileName.endsWith(ext) || fileName.includes(ext.replace('.', ''))) {
+      return extensionToLanguageMap[ext];
+    }
+  }
+  return 'plaintext';
+}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between d79717a and c9b7281.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • web/components/Logs/LogViewer.ce.vue (1 hunks)
  • web/components/Logs/SingleLogViewer.vue (1 hunks)
  • web/package.json (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Build Web App
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (9)
web/package.json (1)

95-95: No immediate concerns with the new highlight.js dependency.
This library is well-suited for syntax highlighting.

web/components/Logs/LogViewer.ce.vue (4)

1-36: Solid setup logic and references.
The reactive approach for selected files, line count, and auto-scroll is straightforward and maintainable.


37-80: Computed properties and helpers look great.
Your logFiles computation and formatFileSize utility are well-organized and clear.


90-173: Comprehensive template structure.
The approach for displaying file selection, line count, syntax highlight options, and error states provides a good user experience.


175-179: Styles placeholder noted.
No issues here; the partial style block is understandable for future expansions.

web/components/Logs/SingleLogViewer.vue (4)

1-43: Good setup for highlight.js and theming.
Registering languages and referencing the theme store for dark mode is a nice touch.


44-179: Robust GraphQL integration.
Using query + subscription ensures real-time updates. The chunk-based loading strategy is well-structured.


181-351: Effective syntax highlighting strategy.
Kudos for including sanitation (DOMPurify), error/warning highlighting, and advanced markers for IPs and timestamps. These additions significantly enhance readability.


353-572: Well-organized layout and custom themes.
Your light/dark theme CSS variables provide a clean, maintainable approach for color-coding logs.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (6)
web/components/Logs/SingleLogViewer.vue (6)

54-68: Consider initializing currentStartLine with a specific value.

Instead of using undefined, it might be clearer to initialize currentStartLine with a specific value like null or 1 to make the intent more explicit.


180-247: Consider extracting highlight patterns to constants.

The regex patterns used for highlighting timestamps and IP addresses could be extracted to named constants to improve readability and maintainability.

+// Regex patterns for log highlighting
+const TIMESTAMP_PATTERN = /\b(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)\b/g;
+const IP_ADDRESS_PATTERN = /\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/g;
+const ERROR_PATTERN = /\b(error|exception|fail|failed|failure)\b/gi;
+const WARNING_PATTERN = /\b(warning|warn)\b/gi;
+const SUCCESS_PATTERN = /\b(success|successful|completed|done)\b/gi;

 const highlightLog = (content: string): string => {
   try {
     // Determine which language to use for highlighting
     const language = props.highlightLanguage || defaultLanguage;
     
     // Apply syntax highlighting
     let highlighted = hljs.highlight(content, { language }).value;
     
     // Apply additional custom highlighting for common log patterns
     
     // Highlight timestamps (various formats)
     highlighted = highlighted.replace(
-      /\b(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)\b/g,
+      TIMESTAMP_PATTERN,
       '<span class="hljs-timestamp">$1</span>'
     );
     
     // Highlight IP addresses
     highlighted = highlighted.replace(
-      /\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/g,
+      IP_ADDRESS_PATTERN,
       '<span class="hljs-ip">$1</span>'
     );

208-235: Consider optimizing the highlighting logic.

The current implementation tests each line multiple times with different regex patterns. This could be optimized by using a single pass approach or by checking for the most common patterns first.

 // Process each line to add error, warning, and success highlighting
 lines = lines.map(line => {
-  if (/(error|exception|fail|failed|failure)/i.test(line)) {
+  // Get line type with a single regex test
+  const isError = ERROR_PATTERN.test(line);
+  const isWarning = !isError && WARNING_PATTERN.test(line);
+  const isSuccess = !isError && !isWarning && SUCCESS_PATTERN.test(line);
+  
+  if (isError) {
     // Highlight error keywords
     line = line.replace(
-      /\b(error|exception|fail|failed|failure)\b/gi,
+      ERROR_PATTERN,
       '<span class="hljs-error-keyword">$1</span>'
     );
     // Wrap the entire line
     return `<span class="hljs-error">${line}</span>`;
-  } else if (/(warning|warn)/i.test(line)) {
+  } else if (isWarning) {
     // Highlight warning keywords
     line = line.replace(
-      /\b(warning|warn)\b/gi,
+      WARNING_PATTERN,
       '<span class="hljs-warning-keyword">$1</span>'
     );
     // Wrap the entire line
     return `<span class="hljs-warning">${line}</span>`;
-  } else if (/(success|successful|completed|done)/i.test(line)) {
+  } else if (isSuccess) {
     // Highlight success keywords
     line = line.replace(
-      /\b(success|successful|completed|done)\b/gi,
+      SUCCESS_PATTERN,
       '<span class="hljs-success-keyword">$1</span>'
     );
     // Wrap the entire line
     return `<span class="hljs-success">${line}</span>`;
   }
   return line;
 });

288-333: Consider adding a file size check before downloading.

For very large log files, it might be helpful to warn users before downloading to prevent browser issues. Consider adding a size check or limiting the download size.

 const downloadLogFile = async () => {
   if (!props.logFilePath || state.isDownloading) return;
   
   try {
     state.isDownloading = true;
     
     // Get the filename from the path
     const fileName = props.logFilePath.split('/').pop() || 'log.txt';
     
+    // First check file size
+    const sizeResult = await client.query({
+      query: GET_LOG_FILE_SIZE, // You would need to create this query
+      variables: {
+        path: props.logFilePath,
+      },
+      fetchPolicy: 'network-only',
+    });
+    
+    const fileSizeBytes = sizeResult.data?.logFile?.sizeBytes || 0;
+    const fileSizeMB = fileSizeBytes / (1024 * 1024);
+    
+    // Warn user if file is large
+    if (fileSizeMB > 10) { // 10MB threshold
+      const confirmDownload = window.confirm(
+        `The log file is ${fileSizeMB.toFixed(2)}MB in size. Do you want to continue with the download?`
+      );
+      if (!confirmDownload) {
+        state.isDownloading = false;
+        return;
+      }
+    }
+    
     // Query for the entire log file content
     const result = await client.query({
       query: GET_LOG_FILE_CONTENT,

82-82: Consider providing a default value for startLine.

When state.currentStartLine is undefined, it's passed directly to the GraphQL query. It might be clearer to use a nullish coalescing operator to provide a default value.

-    startLine: state.currentStartLine,
+    startLine: state.currentStartLine ?? undefined,

169-169: Consider explaining the purpose of the setTimeout delay.

The 300ms timeout might not be immediately clear to someone reading the code. A comment explaining why this delay is necessary would improve maintainability.

-        setTimeout(() => (state.canLoadMore = true), 300);
+        // Add a small delay before allowing more content to be loaded to prevent
+        // accidental double-loading and ensure the UI has stabilized
+        setTimeout(() => (state.canLoadMore = true), 300);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between c9b7281 and 796cb48.

📒 Files selected for processing (3)
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/LogViewer.page (1 hunks)
  • web/components/Logs/SingleLogViewer.vue (1 hunks)
  • web/nuxt.config.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Build Web App
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (10)
web/nuxt.config.ts (1)

145-148: Custom element registration looks good.

The new UnraidLogViewer custom element is properly registered following the project's established pattern.

plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/LogViewer.page (2)

1-4: Page metadata looks good.

The menu placement, title, icon, and tag are properly defined for the Log Viewer page.


17-19: Well-structured component usage.

The page correctly wraps the log viewer component with the i18n host for internationalization support.

web/components/Logs/SingleLogViewer.vue (7)

1-27: Well-structured imports and setup.

The component has a clear organization of imports, properly separating framework utilities from custom components and domain-specific code.


28-43: Good language support for syntax highlighting.

The component registers multiple languages for highlighting, which provides good coverage for various log file formats.


44-53: Props are well-defined with appropriate types.

The TypeScript interface for props is clear and includes documentation comments. The optional highlightLanguage prop provides good flexibility.


98-142: Observer setup logic looks good.

Good use of MutationObserver for tracking content changes and implementing auto-scroll functionality. The subscription setup is also well implemented.


259-285: Solid implementation of loading older content.

The implementation correctly handles the loading state, preserves scroll position, and sets appropriate flags. Good use of async/await for handling asynchronous operations.


354-401: UI layout is well-structured and provides good user feedback.

The template has a clear structure with appropriate loading indicators, error states, and user controls. Good use of infinite scrolling directive.


403-570: Well-organized CSS with theming support.

The CSS uses variables effectively to support both light and dark themes. The highlighting styles are comprehensive and visually distinguishable.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (8)
api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts (2)

9-21: No additional error handling on query.
Currently, the logFiles query delegates error handling to listLogFiles(). This is acceptable; however, if the service throws unexpected exceptions, the error may bubble up here and cause a generic error response. Consider adding a try/catch for more fine-grained control over errors and a clearer user-facing message.


23-35: Add optional input validation for large or invalid line parameters.
When clients request too many lines or pass invalid values for lines or startLine, it may degrade performance or cause edge-case errors. You could add basic bounds checks (e.g., no negative lines, upper limit constraints) to ensure stable behavior.

api/src/unraid-api/graph/resolvers/logs/logs.service.ts (2)

42-66: Potential performance overhead when listing a large number of logs.
The current logic loops through every file in the directory and calls stat on each. For directories containing thousands of logs, this might be slow. Consider batching file stats or using OS-level aggregated info (if available) for better performance.


125-205: Multiple subscriptions handled by a single watcher.
If multiple watchers are unavoidably started for the same file, concurrency or double-publishing could occur. Alternatively, if you rely on one watcher per file, consider locking or carefully synchronizing the read and publish steps to avoid race conditions.

web/components/Logs/SingleLogViewer.vue (4)

33-43: Consider optimizing language registrations with lazy-loading.
Registering all languages at once can increase the bundle size. Especially if users rarely view certain file types, consider dynamically importing only the languages needed at runtime.


57-65: Monitor memory usage when storing large logs in memory.
When dealing with very large logs, repeatedly storing chunks in loadedContentChunks could grow quickly and impact memory usage. Consider an upper limit or periodic truncation if logs are extremely large.


174-177: Relying on a 300ms timeout for enabling canLoadMore can be brittle.
Using a timeout-based approach for re-enabling canLoadMore may fail in some edge cases. Consider hooking into a reliable event or a post-fetch callback to manage state changes more deterministically.


335-337: Enhance user feedback by replacing browser alerts with a UI-based error display.
Relying on alert() can disrupt the user flow and feels inconsistent with modern web UX patterns. Transitioning to a dedicated error toast or modal would improve user experience.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 796cb48 and f27223e.

📒 Files selected for processing (4)
  • api/src/__test__/store/modules/paths.test.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/logs/logs.service.ts (1 hunks)
  • web/components/Logs/SingleLogViewer.vue (1 hunks)
🧰 Additional context used
🪛 GitHub Check: Test API
api/src/__test__/store/modules/paths.test.ts

[failure] 3-3:
Replace ⏎⏎import·{·store·}·from·'@app/store/index.js';⏎⏎⏎⏎ with import·{·store·}·from·'@app/store/index.js';


[failure] 3-3:
More than 1 blank line not allowed


[failure] 7-7:
More than 1 blank line not allowed


[failure] 40-40:
Insert

🪛 GitHub Check: Build API
api/src/__test__/store/modules/paths.test.ts

[failure] 3-3:
Replace ⏎⏎import·{·store·}·from·'@app/store/index.js';⏎⏎⏎⏎ with import·{·store·}·from·'@app/store/index.js';


[failure] 3-3:
More than 1 blank line not allowed


[failure] 7-7:
More than 1 blank line not allowed


[failure] 40-40:
Insert

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Build Web App
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (3)
api/src/__test__/store/modules/paths.test.ts (1)

34-34: LGTM! Addition of "unraid-log-base" path is appropriate.

This addition correctly aligns with the new configuration path that was added for log file management, as mentioned in the PR objectives.

api/src/unraid-api/graph/resolvers/logs/logs.service.ts (1)

253-282: Reading the last N lines may be optimized.
This approach reads and counts every line from the beginning. As previously noted, a reverse seek or chunk-based approach could drastically reduce I/O for large logs.

web/components/Logs/SingleLogViewer.vue (1)

104-109: Be aware of performance implications of MutationObserver in large logs.
The observer will fire for every DOM change in the log content, which might impact performance heavily if logs grow or update frequently. Throttling or batching updates could help.

Comment on lines +37 to +65
@Subscription('logFile')
@UsePermissions({
action: AuthActionVerb.READ,
resource: Resource.LOGS,
possession: AuthPossession.ANY,
})
async logFileSubscription(@Args('path') path: string) {
// Start watching the file
this.logsService.getLogFileSubscriptionChannel(path);

// Create the async iterator
const asyncIterator = createSubscription(PUBSUB_CHANNEL.LOG_FILE);

// Store the original return method to wrap it
const originalReturn = asyncIterator.return;

// Override the return method to clean up resources
asyncIterator.return = async () => {
// Stop watching the file when subscription ends
this.logsService.stopWatchingLogFile(path);

// Call the original return method
return originalReturn
? originalReturn.call(asyncIterator)
: Promise.resolve({ value: undefined, done: true });
};

return asyncIterator;
}
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Watchers for multiple subscriptions may conflict.
Each new subscription starts a file watcher if one doesn't already exist. When the subscription ends, you unconditionally stop watching. This may disrupt other simultaneous subscribers to the same file. Consider implementing a reference counter or subscription tracking to ensure the file watcher remains active until all consumers have unsubscribed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've fixed this inside of the stopWatchingLogFile function

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Let's examine the `stopWatchingLogFile` function in LogsService
cat api/src/unraid-api/graph/resolvers/logs/logs.service.ts | grep -A 30 "stopWatchingLogFile"

Length of output: 1309


@elibosley I see you've addressed the subscription issue by implementing a reference counting mechanism in the stopWatchingLogFile function. The function now decrements a counter and only closes the watcher when all subscribers have unsubscribed (count <= 0). This is a good solution that maintains the watcher for multiple concurrent subscribers.


✏️ Learnings added
Learnt from: elibosley
PR: unraid/api#1222
File: api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts:37-65
Timestamp: 2025-03-17T13:41:52.251Z
Learning: The LogsService in Unraid API uses a reference counting mechanism to track log file subscriptions, ensuring file watchers remain active until all subscribers have unsubscribed.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
api/src/unraid-api/graph/resolvers/logs/logs.resolver.spec.ts (1)

11-13: ⚠️ Potential issue

Missing LogsService dependency in test setup.

Based on the AI summary and previous review, the LogsResolver depends on LogsService, but it's not included in the test module. This will likely cause the test to fail as the resolver won't be able to be properly instantiated.

Update the test module to include a mock for LogsService:

 const module: TestingModule = await Test.createTestingModule({
-    providers: [LogsResolver],
+    providers: [
+        LogsResolver,
+        {
+            provide: LogsService,
+            useValue: {
+                // Add mock implementations for service methods used by resolver
+            }
+        }
+    ],
 }).compile();

Don't forget to add the import:

 import { Test, TestingModule } from '@nestjs/testing';
 import { beforeEach, describe, expect, it } from 'vitest';
 import { LogsResolver } from '@app/unraid-api/graph/resolvers/logs/logs.resolver.js';
+import { LogsService } from '@app/unraid-api/graph/resolvers/logs/logs.service.js';
🧹 Nitpick comments (2)
api/src/unraid-api/graph/resolvers/logs/logs.resolver.spec.ts (2)

7-21: Test suite lacks coverage for resolver methods.

The test suite only verifies that the resolver can be instantiated but doesn't test any of its actual functionality. Consider adding tests for each resolver method to ensure they work as expected.

Example test cases you might want to add:

  • Tests for log file listing functionality
  • Tests for log file content retrieval
  • Tests for subscription handlers

For each test, you'll need to set up appropriate mocks for the LogsService methods and verify the resolver correctly processes and returns the data.


1-3: Separate imports by source type.

Consider organizing imports by grouping them according to their source: external libraries first, then internal modules, and finally relative imports.

 import { Test, TestingModule } from '@nestjs/testing';
-
 import { beforeEach, describe, expect, it } from 'vitest';
+
 import { LogsResolver } from '@app/unraid-api/graph/resolvers/logs/logs.resolver.js';
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 82558af and b8ac90c.

📒 Files selected for processing (1)
  • api/src/unraid-api/graph/resolvers/logs/logs.resolver.spec.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Build Web App
  • GitHub Check: Build API
  • GitHub Check: Test API
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (1)
api/src/unraid-api/graph/resolvers/logs/logs.resolver.spec.ts (1)

5-5: Verify import file extension.

The import statement includes the .js extension, which is unusual for TypeScript files. Make sure this is intentional and follows your project's module resolution configuration.

If your project uses ESM modules with TypeScript, the .js extension might be necessary. Otherwise, you might want to remove it:

-import { LogsResolver } from '@app/unraid-api/graph/resolvers/logs/logs.resolver.js';
+import { LogsResolver } from '@app/unraid-api/graph/resolvers/logs/logs.resolver';

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
api/src/unraid-api/graph/resolvers/logs/logs.service.ts (3)

45-69: Rethink silent failure approach.
listLogFiles() logs the error but returns an empty array, which can mask critical filesystem issues from upstream callers. Consider rethrowing or returning a specialized error value so callers can handle the situation.


137-219: Gracefully handle file rename or removal.
Currently, if the file is removed or renamed, you may continue watching a non-existent path. Monitoring for unlink events (or verifying file existence upon change events) could enhance robustness.


308-354: Consider a more efficient strategy for readLinesFromPosition.
While functional, scanning line-by-line from the start is expensive for large startLine values. A partial seek or chunk-based approach may help optimize performance.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between b8ac90c and 4758d2c.

📒 Files selected for processing (2)
  • api/src/unraid-api/graph/resolvers/logs/logs.resolver.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/logs/logs.service.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/src/unraid-api/graph/resolvers/logs/logs.resolver.spec.ts
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Build Web App
  • GitHub Check: Build API
  • GitHub Check: Test API
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (5)
api/src/unraid-api/graph/resolvers/logs/logs.service.ts (5)

1-11: Consider validating symlinks for added security.
While using basename(path) prevents direct directory traversal, attackers could create symlinks in your log directory that point outside. Consider resolving real paths or disallowing symlinks to strengthen security.

Would you like a follow-up script to search for symbolic link usage in your codebase?


77-111: Ensure usage alignment when startLine exceeds total lines.
If startLine is larger than the total line count, the returned content is empty. Verify this behavior is intended and that callers correctly handle edge cases.


117-135: Looks good for subscription management.
This approach to maintain watchers in a map with subscription counts is well-organized and prevents multiple watchers on the same file.


245-270: Reading the entire file to count lines can be expensive.
This was raised previously: for large files, consider a more optimal approach (e.g., reading in reverse).


272-306: Large-file performance caution (repeat suggestion).
Like countFileLines, readLastLines processes the entire file, which can become costly. A reverse-seek approach could significantly reduce I/O overhead.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
web/components/Logs/LogViewer.ce.vue (2)

56-80: Consider refactoring the language detection function

The auto-detection logic works but could be more maintainable with a map-based approach rather than a long series of conditionals.

-const autoDetectLanguage = (filePath: string): string => {
-  const fileName = filePath.split('/').pop() || '';
-  
-  if (fileName.endsWith('.sh') || fileName.endsWith('.bash') || fileName.includes('syslog')) {
-    return 'bash';
-  } else if (fileName.endsWith('.conf') || fileName.endsWith('.ini') || fileName.endsWith('.cfg')) {
-    return 'ini';
-  } else if (fileName.endsWith('.xml') || fileName.endsWith('.html')) {
-    return 'xml';
-  } else if (fileName.endsWith('.json')) {
-    return 'json';
-  } else if (fileName.endsWith('.yml') || fileName.endsWith('.yaml')) {
-    return 'yaml';
-  } else if (fileName.includes('nginx')) {
-    return 'nginx';
-  } else if (fileName.includes('apache') || fileName.includes('httpd')) {
-    return 'apache';
-  } else if (fileName.endsWith('.js')) {
-    return 'javascript';
-  } else if (fileName.endsWith('.php')) {
-    return 'php';
-  }
-  
-  return 'plaintext';
-};
+const autoDetectLanguage = (filePath: string): string => {
+  const fileName = filePath.split('/').pop() || '';
+  
+  const extensionMap: Record<string, string> = {
+    '.sh': 'bash',
+    '.bash': 'bash',
+    '.conf': 'ini',
+    '.ini': 'ini',
+    '.cfg': 'ini',
+    '.xml': 'xml',
+    '.html': 'xml',
+    '.json': 'json',
+    '.yml': 'yaml',
+    '.yaml': 'yaml',
+    '.js': 'javascript',
+    '.php': 'php'
+  };
+  
+  const keywordMap: Record<string, string> = {
+    'syslog': 'bash',
+    'nginx': 'nginx',
+    'apache': 'apache',
+    'httpd': 'apache'
+  };
+  
+  // Check for file extension matches
+  for (const [ext, lang] of Object.entries(extensionMap)) {
+    if (fileName.endsWith(ext)) {
+      return lang;
+    }
+  }
+  
+  // Check for keyword matches in filename
+  for (const [keyword, lang] of Object.entries(keywordMap)) {
+    if (fileName.includes(keyword)) {
+      return lang;
+    }
+  }
+  
+  return 'plaintext';
+};

110-120: Consider adding validation for line count

While you have min and max attributes on the input, consider adding explicit validation to ensure the value remains within bounds even when manually edited.

 <div>
   <Label for="line-count">Lines</Label>
   <Input 
     id="line-count" 
-    v-model.number="lineCount" 
+    v-model.number="lineCount"
+    @update:modelValue="val => lineCount = Math.min(1000, Math.max(10, val))"
     type="number" 
     min="10" 
     max="1000" 
     class="w-24"
   />
 </div>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 5f79370 and 75ebe85.

📒 Files selected for processing (1)
  • web/components/Logs/LogViewer.ce.vue (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Build Web App
  • GitHub Check: Test API
  • GitHub Check: Build API
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (8)
web/components/Logs/LogViewer.ce.vue (8)

17-21: State management looks good

The component state is well-defined with appropriate defaults. The use of Vue refs for reactive state management follows best practices.


23-35: Good selection of highlight languages

The list of supported syntax highlighting languages covers most common log file formats. This provides good flexibility for users.


37-42: Apollo query implementation is correct

Good use of Apollo composables with proper handling of loading state, error state, and results.


45-53: File size formatting implementation is solid

The utility function handles all edge cases and properly formats file sizes with appropriate units.


82-87: Good use of watch effect

The watch effect for auto-detecting language is well implemented and properly handles selection changes.


90-144: Well-structured UI controls

The header section is well-organized with appropriate controls for file selection, line count, syntax highlighting, and auto-scroll options.


146-172: Good handling of different states

Excellent job implementing conditional rendering for various states (loading, error, empty, etc.). This provides clear feedback to the user.


175-180: Styles are properly imported

Good practice to import UI framework styles before custom styles.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
web/components/Logs/LogViewer.ce.vue (4)

44-53: Consider Supporting Larger File Sizes
Your formatFileSize function currently stops at gigabytes. If there's a scenario where logs can exceed gigabytes, you might want to extend support to terabytes or beyond.


55-80: Refine Filename Matching for Language Detection
You are using partial substring checks (e.g. fileName.includes('apache')). Consider a stricter approach like checking file extensions or known patterns to avoid false matches.


97-107: Match 'for' and 'id' for Better Accessibility
You have <Label for="log-file-select"> but haven't defined id="log-file-select" on the corresponding <Select>. Consider adding an id to improve screen reader support.


151-153: Review Error Message Exposure
Rendering the full error message can inadvertently disclose stack traces or internal details in production. Verify that this is the intended behavior, or sanitize the error content if needed.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 75ebe85 and 60aeddb.

📒 Files selected for processing (1)
  • web/components/Logs/LogViewer.ce.vue (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Test API
  • GitHub Check: Build Web App
  • GitHub Check: Build API
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (1)
web/components/Logs/LogViewer.ce.vue (1)

1-13: Solid Composition API Setup
This section cleanly sets up Composition API with TypeScript and uses Apollo integrators in a straightforward manner. Good job!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
web/components/Logs/LogViewer.ce.vue (3)

25-37: Consider externalizing highlight languages for maintainability.

Listing them directly in the code can be unwieldy if you add more syntaxes or need to update labels. Extracting them into a shared config or utility can make the code more scalable.


125-135: Reconsider the line count limits for heavy log usage.

Restricting the user to 1,000 lines might be too limiting in some scenarios. Allowing dynamic pagination or higher limits can enhance usability for large logs.


187-194: Consider partial loading or pagination for huge logs.

Loading the entire file at once can lead to performance and memory issues with very large logs. Introducing pagination or smaller chunk retrieval will improve responsiveness.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 60aeddb and 85b6edd.

📒 Files selected for processing (1)
  • web/components/Logs/LogViewer.ce.vue (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Build Web App
  • GitHub Check: Test API
  • GitHub Check: Build API
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (1)
web/components/Logs/LogViewer.ce.vue (1)

61-86: Add more patterns or fallback for unrecognized file extensions.

Currently, files like “.txt” or “.log” default to plaintext, which may not be optimal for scenarios involving specialized logs. Please verify whether you expect other file types and adjust the detection logic accordingly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
web/components/Logs/LogViewer.ce.vue (4)

1-1: Consider adding a test suite for the new LogViewer component.
While the code appears solid, implementing a small integration or end-to-end test would help ensure reliability, especially when future changes impact log viewing functionality.

Do you want me to open a new issue or draft a test example for you?


52-60: Consider handling file sizes larger than 1 GB.
Your current formatFileSize function doesn’t explicitly account for TB or PB scenarios, which might be relevant if logs grow significantly.


63-87: Add explicit handling for .log extensions if desired.
A .log file currently defaults to plaintext. If most logs have a .log extension, you may consider mapping them to a specialized syntax.


111-122: Potentially refine for large log file lists.
If the number of log files grows, consider pagination or lazy loading to maintain responsiveness.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between afa3b5a and 23132b4.

📒 Files selected for processing (1)
  • web/components/Logs/LogViewer.ce.vue (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Build Web App
  • GitHub Check: Build API
  • GitHub Check: Test API
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (4)
web/components/Logs/LogViewer.ce.vue (4)

14-15: Kudos on properly importing useTeleport.
This resolves the past reference error concern and ensures the dropdown can be teleported without runtime issues.


90-94: Reconfirm auto-detect behavior vs. user choice.
Auto-detecting the highlight language on file change might override a user’s custom selection. Ensure this is intended.


97-101: Teleport usage looks correct.
This approach prevents rendering issues for the dropdown in certain contexts.


200-204: Global styles appear well-integrated.
Including @unraid/ui/styles and your base CSS fosters consistency across the app.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
web/components/Logs/LogViewer.ce.vue (2)

62-87: Consider adding .log extension detection.
The auto-detection logic currently skips .log files. If you often handle .log extensions, consider mapping them to 'plaintext' or a relevant language.

} else if (fileName.endsWith('.log')) {
  return 'plaintext';
}

110-122: Guard against no matching file in the dropdown.
If selectedLogFile is no longer present in logFiles, you might want to reset selectedLogFile or handle it gracefully.

web/components/Logs/SingleLogViewer.vue (2)

124-134: Potential large in-memory usage.
Appending new lines to lastChunk.content can build a large string if logs are very long. Consider a chunked approach or a maximum buffer size to prevent excessive memory usage.


295-337: Replace alert with a more consistent UI notification.
Using a plain alert might disrupt the UX. A toast or an inline error message might be more user-friendly.

- alert(`Error downloading log file: ${error instanceof Error ? error.message : String(error)}`);
+ // Example toast or inline error approach
+ // showToast(`Error downloading log file: ${error instanceof Error ? error.message : String(error)}`, 'error');
web/composables/gql/graphql.ts (1)

610-611: Check if 32-bit integer is sufficient for file size.
The size field is declared as an Int, but some logs could exceed the 2GB boundary. Consider using a 64-bit type (e.g., Long) for more robust support of large log files.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 23132b4 and f3f84e9.

⛔ Files ignored due to path filters (3)
  • api/src/graphql/generated/api/operations.ts is excluded by !**/generated/**
  • api/src/graphql/generated/api/types.ts is excluded by !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • api/package.json (4 hunks)
  • api/src/__test__/store/modules/paths.test.ts (1 hunks)
  • api/src/core/pubsub.ts (1 hunks)
  • api/src/graphql/schema/types/logs/logs.graphql (1 hunks)
  • api/src/store/modules/paths.ts (1 hunks)
  • api/src/unraid-api/auth/user.decorator.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/logs/logs.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/logs/logs.resolver.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/logs/logs.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts (2 hunks)
  • api/src/unraid-api/main.ts (1 hunks)
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/LogViewer.page (1 hunks)
  • web/components/Logs/LogViewer.ce.vue (1 hunks)
  • web/components/Logs/SingleLogViewer.vue (1 hunks)
  • web/components/Logs/log.query.ts (1 hunks)
  • web/components/Logs/log.subscription.ts (1 hunks)
  • web/composables/gql/gql.ts (3 hunks)
  • web/composables/gql/graphql.ts (7 hunks)
  • web/nuxt.config.ts (1 hunks)
  • web/package.json (1 hunks)
  • web/pages/index.vue (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (17)
  • api/src/test/store/modules/paths.test.ts
  • web/components/Logs/log.subscription.ts
  • web/components/Logs/log.query.ts
  • api/src/unraid-api/graph/resolvers/logs/logs.module.ts
  • api/src/unraid-api/main.ts
  • api/src/core/pubsub.ts
  • web/pages/index.vue
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/LogViewer.page
  • api/src/unraid-api/graph/resolvers/logs/logs.resolver.spec.ts
  • web/nuxt.config.ts
  • web/package.json
  • api/src/store/modules/paths.ts
  • api/src/unraid-api/auth/user.decorator.ts
  • api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts
  • api/package.json
  • api/src/graphql/schema/types/logs/logs.graphql
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (14)
web/components/Logs/LogViewer.ce.vue (2)

14-15: Thanks for importing useTeleport properly!
Everything looks correct, and it addresses the issue mentioned in the past review about reference errors.


96-101: Teleport usage is good.
Your logic of calling determineTeleportTarget() on open ensures the dropdown sees a valid teleport root.

web/components/Logs/SingleLogViewer.vue (1)

90-107: Good approach for auto-scroll via MutationObserver.
This ensures the user stays at the bottom when new lines arrive. No issues found.

web/composables/gql/graphql.ts (1)

1800-1820: GraphQL queries and subscriptions look accurate.
These additions align well with the new log file schema objects. Good job!

api/src/unraid-api/graph/resolvers/logs/logs.service.ts (7)

45-69: Clean implementation of file listing

The listLogFiles method looks well-implemented with proper error handling and clear object mapping.


77-111: Path validation is correctly implemented

Good use of join and basename to validate the path is within the log directory. This prevents path traversal vulnerabilities.


136-219: Good resource management for file watchers

The implementation properly handles file watchers with subscription counting and cleanup. The chokidar configuration with the awaitWriteFinish options helps prevent partial file reads during active writes.


225-243: Well-designed cleanup mechanism

The reference counting approach for file watchers ensures resources are properly cleaned up when no more clients are subscribed.


277-306: Reading last N lines may be optimized

Currently, the entire file is read twice: once to count lines and then again to read the last N lines. For large log files, this approach will be inefficient.

Consider implementing a more efficient approach using a reverse reading strategy or a bounded buffer:

private async readLastLines(filePath: string, lineCount: number): Promise<string> {
-    const totalLines = await this.countFileLines(filePath);
-    const linesToSkip = Math.max(0, totalLines - lineCount);
-
-    return new Promise((resolve, reject) => {
-        let currentLine = 0;
-        let content = '';
-
-        const stream = createReadStream(filePath);
-        const rl = createInterface({
-            input: stream,
-            crlfDelay: Infinity,
-        });
-
-        rl.on('line', (line) => {
-            currentLine++;
-            if (currentLine > linesToSkip) {
-                content += line + '\n';
-            }
-        });
-
-        rl.on('close', () => {
-            resolve(content);
-        });
-
-        rl.on('error', (err) => {
-            reject(err);
-        });
-    });
+    return new Promise(async (resolve, reject) => {
+        try {
+            // Get file size
+            const fileStats = await stat(filePath);
+            if (fileStats.size === 0) {
+                return resolve('');
+            }
+            
+            // Use a circular buffer to store the last N lines
+            const buffer = new Array(lineCount);
+            let lineCounter = 0;
+            
+            const stream = createReadStream(filePath);
+            const rl = createInterface({
+                input: stream,
+                crlfDelay: Infinity,
+            });
+            
+            rl.on('line', (line) => {
+                buffer[lineCounter % lineCount] = line;
+                lineCounter++;
+            });
+            
+            rl.on('close', () => {
+                // Reconstruct content from circular buffer
+                let content = '';
+                const linesRead = Math.min(lineCounter, lineCount);
+                const startIndex = lineCounter < lineCount ? 0 : lineCounter % lineCount;
+                
+                for (let i = 0; i < linesRead; i++) {
+                    const index = (startIndex + i) % lineCount;
+                    content += buffer[index] + '\n';
+                }
+                
+                resolve(content);
+            });
+            
+            rl.on('error', (err) => {
+                reject(err);
+            });
+        } catch (error) {
+            reject(error);
+        }
+    });
}

314-354: Efficient implementation for reading from position

The readLinesFromPosition method efficiently stops reading once it has read the required number of lines.


38-40:

✅ Verification successful

Verify the path configuration exists

Make sure the 'unraid-log-base' path configuration exists in the system. If it doesn't, this service will fail to function properly.


🏁 Script executed:

#!/bin/bash
# Check if the unraid-log-base path is defined and accessible
rg -A 5 "unraid-log-base" 

Length of output: 1396


Path configuration verified
The 'unraid-log-base' configuration is confirmed—it is defined in api/src/store/modules/paths.ts as unraid-log-base: resolvePath('/var/log/' as const). This ensures that the service in logs.service.ts works as expected.

web/composables/gql/gql.ts (3)

17-19: GraphQL operations for log files properly defined

The GraphQL queries and subscription for log files are correctly defined with all necessary fields.


41-43: Document mapping is correctly implemented

The documents mapping correctly connects the GraphQL query strings to their respective TypeScript types.


79-90: GraphQL function overloads for log operations

The GraphQL function overloads for log operations are correctly implemented, ensuring proper type safety.

pujitm
pujitm previously approved these changes Mar 17, 2025
Copy link
Member

@pujitm pujitm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, working great on my server. one q/followup tho: think it'd be possible/simple to replace escapes with colors? e.g. from graphql-api.log:
image

@github-actions
Copy link
Contributor

This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL:

https://preview.dl.unraid.net/unraid-api/tag/PR1222/dynamix.unraid.net.plg

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (9)
web/components/Logs/LogViewer.ce.vue (3)

52-60: Suggest adding unit tests for formatFileSize.

Although the helper function correctly computes file sizes for typical scenarios, consider adding basic unit tests to verify edge cases (e.g. extremely large numbers, small fractional values, etc.).


62-87: Consider expanding auto-detection logic for additional extensions.

Right now, autoDetectLanguage covers many file types but misses others (e.g. .py). If relevant to your application, you could easily extend or parameterize the detection to handle more cases in the future.


104-198: Add test coverage for user interactions and error states.

The template properly handles loading, errors, empty logs, and no-file selection. However, it’s best to capture these flows in your end-to-end or component tests to ensure no regressions.

Would you like help drafting a quick test suite for these states?

web/components/Logs/SingleLogViewer.vue (3)

44-49: Ensure prop validations for logFilePath and lineCount.

Currently, the component expects these props but doesn't enforce validations (e.g. non-empty strings for logFilePath, numeric range for lineCount). Consider using runtime or compile-time prop validations to catch invalid usage.


186-254: Performance caution with large log content and highlighting.

While the chunk-based approach mitigates some memory concerns, repeated calls to syntax highlighting for very large logs could be costly. If logs can be huge, you may need additional pagination or streaming solutions to prevent a large memory footprint and slow rendering.


336-337: Replace bare alerts with a custom error notification.

Using alert() is functional but can disrupt the user experience. A dedicated notification component or toast system might better integrate with the rest of your UI and preserve the user’s context.

web/composables/gql/graphql.ts (1)

1160-1168: Allow for optional filters or pagination if logs grow in quantity.

Currently, logFiles returns all files without pagination. If the number of log files is large, adding filtering or pagination arguments may improve performance for queries over time.

api/src/unraid-api/graph/resolvers/logs/logs.service.ts (2)

45-69: Ensure large directory handling is efficient.
Listing all files in the directory, followed by individually calling stat on each file, may be expensive if the directory contains many files. For large log directories, consider batching I/O operations or implementing a pagination approach to avoid potential performance bottlenecks.


249-270: Optimize line-counting for large files.
Reading an entire file line-by-line for counting can be time-consuming for large logs. Consider a chunk-based approach or optional OS-level utilities for faster results—especially on frequently accessed logs.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between f3f84e9 and b13b209.

⛔ Files ignored due to path filters (3)
  • api/src/graphql/generated/api/operations.ts is excluded by !**/generated/**
  • api/src/graphql/generated/api/types.ts is excluded by !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (19)
  • api/src/__test__/store/modules/paths.test.ts (1 hunks)
  • api/src/core/pubsub.ts (1 hunks)
  • api/src/graphql/schema/types/logs/logs.graphql (1 hunks)
  • api/src/store/modules/paths.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/logs/logs.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/logs/logs.resolver.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/logs/logs.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts (2 hunks)
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/LogViewer.page (1 hunks)
  • web/components/Logs/LogViewer.ce.vue (1 hunks)
  • web/components/Logs/SingleLogViewer.vue (1 hunks)
  • web/components/Logs/log.query.ts (1 hunks)
  • web/components/Logs/log.subscription.ts (1 hunks)
  • web/composables/gql/gql.ts (3 hunks)
  • web/composables/gql/graphql.ts (10 hunks)
  • web/nuxt.config.ts (1 hunks)
  • web/package.json (1 hunks)
  • web/pages/index.vue (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (14)
  • api/src/test/store/modules/paths.test.ts
  • api/src/unraid-api/graph/resolvers/logs/logs.module.ts
  • web/package.json
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
  • api/src/store/modules/paths.ts
  • api/src/core/pubsub.ts
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/LogViewer.page
  • web/components/Logs/log.query.ts
  • api/src/unraid-api/graph/resolvers/logs/logs.resolver.spec.ts
  • web/nuxt.config.ts
  • api/src/unraid-api/graph/resolvers/logs/logs.resolver.ts
  • web/pages/index.vue
  • web/components/Logs/log.subscription.ts
  • api/src/graphql/schema/types/logs/logs.graphql
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Build Web App
  • GitHub Check: Test API
  • GitHub Check: Build API
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (10)
web/components/Logs/LogViewer.ce.vue (1)

14-15: Good job importing and leveraging useTeleport.

Importing useTeleport from @unraid/ui ensures the select dropdown can properly display in teleported contexts. This resolves previous discussion about missing imports and avoids runtime errors.

web/components/Logs/SingleLogViewer.vue (1)

99-110: Confirm the observer lifecycle and avoid potential memory leaks.

A MutationObserver is created and observed on mount, but only disconnected on unmount. This design is correct; just confirm all possible mount/unmount cycles are properly handled (e.g. if the component is reused or dynamically mounted multiple times).

web/composables/gql/graphql.ts (2)

651-675: Schema additions for LogFile and LogFileContent look good.

Defining fields like modifiedAt and size ensures you have all the required file metadata. This clear approach is suitable for log file management.


1872-1891: GraphQL document definitions are coherent and align with the schema.

The queries and subscription definitions match the shape of the newly introduced types, ensuring consistency between the client and server.

api/src/unraid-api/graph/resolvers/logs/logs.service.ts (3)

141-219: Consider awaiting the watcher initialization.
this.startWatchingLogFile(normalizedPath); is invoked without await. If subsequent operations rely on the watcher being fully ready, a race condition may occur. Ensure either that you don’t need to rely on immediate readiness or consider using await this.startWatchingLogFile(...).


225-243: Confirm proper watcher cleanup.
Stopping the watcher is contingent on subscription counts reaching zero. This is a sound approach, but verify that all flows calling getLogFileSubscriptionChannel also call stopWatchingLogFile when done, to prevent resource leaks.


277-306: [Duplicate of previous comment] Consider a more performant approach for reading the last N lines.

web/composables/gql/gql.ts (3)

19-21: Good addition for listing log files.
The new LogFiles query helps retrieve log metadata efficiently. No issues found here.


45-47: Maintain consistent naming.
The operation names (LogFiles, LogFileContent, LogFileSubscription) are straightforward. Ensure they remain aligned with the backend resolvers for clarity.


91-102: Overloads align well with existing patterns.
The typed overloads for new queries and subscription fit seamlessly into the existing pattern. This ensures type safety and clarity.

Comment on lines +265 to +293
// Load older log content
const loadMoreContent = async () => {
if (state.isLoadingMore || state.isAtTop || !state.canLoadMore) return;
state.isLoadingMore = true;
state.canLoadMore = false;
const firstChunk = state.loadedContentChunks[0];
if (firstChunk) {
const newStartLine = Math.max(1, firstChunk.startLine - DEFAULT_CHUNK_SIZE);
state.currentStartLine = newStartLine;
const prevScrollHeight = scrollViewportRef.value?.scrollHeight || 0;
await refetchLogContent();
nextTick(() => {
if (scrollViewportRef.value) {
scrollViewportRef.value.scrollTop += scrollViewportRef.value.scrollHeight - prevScrollHeight;
}
});
if (newStartLine === 1) {
state.isAtTop = true;
state.canLoadMore = false;
}
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Unify chunk size with user-defined line counts.

You currently use props.lineCount for the initial fetch but a hardcoded DEFAULT_CHUNK_SIZE of 100 for subsequent loads. For a more consistent user experience, consider making these values sync or configurable.

Here’s a quick snippet illustrating a unified approach:

-const DEFAULT_CHUNK_SIZE = 100;
 ...
-  const newStartLine = Math.max(1, firstChunk.startLine - DEFAULT_CHUNK_SIZE);
+const fetchChunkSize = computed(() => props.lineCount || 100);
 ...
+  const newStartLine = Math.max(1, firstChunk.startLine - fetchChunkSize.value);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Load older log content
const loadMoreContent = async () => {
if (state.isLoadingMore || state.isAtTop || !state.canLoadMore) return;
state.isLoadingMore = true;
state.canLoadMore = false;
const firstChunk = state.loadedContentChunks[0];
if (firstChunk) {
const newStartLine = Math.max(1, firstChunk.startLine - DEFAULT_CHUNK_SIZE);
state.currentStartLine = newStartLine;
const prevScrollHeight = scrollViewportRef.value?.scrollHeight || 0;
await refetchLogContent();
nextTick(() => {
if (scrollViewportRef.value) {
scrollViewportRef.value.scrollTop += scrollViewportRef.value.scrollHeight - prevScrollHeight;
}
});
if (newStartLine === 1) {
state.isAtTop = true;
state.canLoadMore = false;
}
}
};
// Assuming this is at the top of your script or within the component setup:
// Remove or comment out the hardcoded constant if it exists
// const DEFAULT_CHUNK_SIZE = 100;
const fetchChunkSize = computed(() => props.lineCount || 100);
// Load older log content
const loadMoreContent = async () => {
if (state.isLoadingMore || state.isAtTop || !state.canLoadMore) return;
state.isLoadingMore = true;
state.canLoadMore = false;
const firstChunk = state.loadedContentChunks[0];
if (firstChunk) {
- const newStartLine = Math.max(1, firstChunk.startLine - DEFAULT_CHUNK_SIZE);
+ const newStartLine = Math.max(1, firstChunk.startLine - fetchChunkSize.value);
state.currentStartLine = newStartLine;
const prevScrollHeight = scrollViewportRef.value?.scrollHeight || 0;
await refetchLogContent();
nextTick(() => {
if (scrollViewportRef.value) {
scrollViewportRef.value.scrollTop += scrollViewportRef.value.scrollHeight - prevScrollHeight;
}
});
if (newStartLine === 1) {
state.isAtTop = true;
state.canLoadMore = false;
}
}
};

Comment on lines +77 to +111
async getLogFileContent(
path: string,
lines = this.DEFAULT_LINES,
startLine?: number
): Promise<LogFileContent> {
try {
// Validate that the path is within the log directory
const normalizedPath = join(this.logBasePath, basename(path));

// Count total lines
const totalLines = await this.countFileLines(normalizedPath);

let content: string;

if (startLine !== undefined) {
// Read from specific starting line
content = await this.readLinesFromPosition(normalizedPath, startLine, lines);
} else {
// Read the last N lines (default behavior)
content = await this.readLastLines(normalizedPath, lines);
}

return {
path: normalizedPath,
content,
totalLines,
startLine: startLine !== undefined ? startLine : Math.max(1, totalLines - lines + 1),
};
} catch (error: unknown) {
this.logger.error(`Error reading log file: ${error}`);
throw new Error(
`Failed to read log file: ${error instanceof Error ? error.message : String(error)}`
);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Validate path safety and handle large files carefully.
While you defensively normalize the path using basename, validate whether the log directory might contain symbolic links or unexpected file types that could break assumptions. Additionally, for large files, calling countFileLines on each request can be expensive. You might want to consider caching line counts or chunk-based reading if performance becomes an issue.

@elibosley elibosley merged commit 2d90408 into main Mar 17, 2025
8 checks passed
@elibosley elibosley deleted the feat/logs-in-gql branch March 17, 2025 15:44
elibosley added a commit that referenced this pull request Mar 17, 2025
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

- **New Features**
- Introduced a comprehensive Log Viewer accessible from the web
interface and Unraid management, allowing users to easily view, refresh,
and download log files.
- Enabled real-time log updates with auto-scroll functionality for
seamless monitoring.
- Enhanced log display with syntax highlighting and detailed file
metadata for improved readability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
pujitm pushed a commit that referenced this pull request Mar 18, 2025
🤖 I have created a release *beep* *boop*
---


## [4.2.0](v4.1.3...v4.2.0)
(2025-03-18)


### Features

* add resolver for logging
([#1222](#1222))
([2d90408](2d90408))
* connect settings web component
([#1211](#1211))
([653de00](653de00))
* improve local dev with install path
([#1221](#1221))
([32c5b0a](32c5b0a))
* split plugin builds
([4d10966](4d10966))
* swap to absolute paths for css
([#1224](#1224))
([6f9fa10](6f9fa10))
* update theme application logic and color picker
([#1181](#1181))
([c352f49](c352f49))
* use patch version if needed on update check
([#1227](#1227))
([6ed46b3](6ed46b3))


### Bug Fixes

* add INELIGIBLE state to ConfigErrorState enum
([#1220](#1220))
([1f00212](1f00212))
* **api:** dynamix notifications dir during development
([#1216](#1216))
([0a382ca](0a382ca))
* **api:** type imports from generated graphql types
([#1215](#1215))
([fd02297](fd02297))
* **deps:** update dependency @nestjs/schedule to v5
([#1197](#1197))
([b1ff6e5](b1ff6e5))
* **deps:** update dependency @vueuse/core to v12
([#1199](#1199))
([d8b8339](d8b8339))
* fix changelog thing again
([2426345](2426345))
* fix invalid path to node with sh execution
([#1213](#1213))
([d12448d](d12448d))
* load tag correctly
([acd692b](acd692b))
* log errors
([629feda](629feda))
* one-command dev & web env files
([#1214](#1214))
([8218fab](8218fab))
* re-release fixed
([bb526b5](bb526b5))
* recreate watcher on path change
([#1203](#1203))
([5a9154e](5a9154e))
* update brand loading variants for consistent sizing
([#1223](#1223))
([d7a4b98](d7a4b98))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants