AG-Grid-React-Series-#74-Log-Viewer-System-Using-AG-Grid
In the world of software development, logs are indispensable. They are the breadcrumbs that lead us to understand application behavior, diagnose issues, and monitor performance. However, raw log files can be overwhelming. This is where a robust log viewer system comes into play. In this installment of our AG-Grid-React series, we'll explore how to leverage the powerful features of AG-Grid to build an efficient, interactive, and user-friendly log viewer system in a React application.
Building a log viewer with AG-Grid isn't just about displaying data; it's about making that data actionable. We'll cover everything from basic display to advanced features like real-time updates, custom cell rendering for log levels, and effective filtering.
Why AG-Grid for a Log Viewer?
AG-Grid is an excellent choice for a log viewer system due due to its extensive feature set and high performance:
- High Performance: Handles thousands, even millions, of rows with ease thanks to virtualization.
- Filtering & Searching: Built-in text, number, date, and custom filters allow users to quickly narrow down logs.
- Sorting: Sort logs by timestamp, level, message, or any other column.
- Custom Cell Rendering: Customize the appearance of cells, perfect for highlighting log levels (e.g., errors in red, warnings in yellow).
- Real-time Updates: Efficiently add, remove, or update rows without re-rendering the entire grid, crucial for live log streams.
- Column Resizing & Reordering: Users can adjust the display to their preferences.
- Theming: Easily style the grid to match your application's look and feel.
Core Components of Our Log Viewer
Our log viewer will aim to include the following essential features:
- Displaying log entries with relevant columns (timestamp, level, source, message).
- Color-coding log levels for quick visual identification.
- Filtering capabilities for log levels and text search for messages.
- Ability to handle real-time log data streams.
Setting Up Your React Project
First, let's set up a basic React project and install AG-Grid-React:
npx create-react-app ag-grid-log-viewer
cd ag-grid-log-viewer
npm install ag-grid-community ag-grid-react
Basic AG-Grid Integration
Now, let's create a simple App.js that displays some mock log data. We'll define our columns and some initial log entries.
// src/App.js
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/styles/ag-grid.css'; // Core grid CSS
import 'ag-grid-community/styles/ag-theme-alpine.css'; // Theme
const generateLogEntry = (index) => {
const levels = ['INFO', 'WARN', 'ERROR', 'DEBUG'];
const sources = ['AuthService', 'UserService', 'PaymentGateway', 'DataProcessor'];
const messages = [
'User logged in successfully.',
'Failed to connect to database.',
'Processing transaction #12345.',
'Invalid API key provided.',
'Data validation warning.',
'System heartbeat OK.'
];
const timestamp = new Date(Date.now() - (Math.random() * 3600000)).toISOString(); // Last hour
return {
id: index,
timestamp: timestamp,
level: levels[Math.floor(Math.random() * levels.length)],
source: sources[Math.floor(Math.random() * sources.length)],
message: messages[Math.floor(Math.random() * messages.length)],
};
};
function App() {
const gridRef = useRef();
const [rowData, setRowData] = useState([]);
const columnDefs = [
{ field: 'timestamp', headerName: 'Timestamp', width: 200, sortable: true, filter: 'agDateColumnFilter' },
{ field: 'level', headerName: 'Level', width: 100, sortable: true, filter: true }, // Will customize this
{ field: 'source', headerName: 'Source', width: 150, sortable: true, filter: true },
{ field: 'message', headerName: 'Message', flex: 1, sortable: true, filter: true },
];
useEffect(() => {
// Generate some initial logs
const initialLogs = Array.from({ length: 100 }, (_, i) => generateLogEntry(i));
setRowData(initialLogs);
}, []);
const defaultColDef = {
resizable: true,
filter: true,
floatingFilter: true, // Enable floating filters by default
suppressMenu: true,
};
return (
Real-time Log Viewer
params.data.id, [])} // Required for efficient row updates
/>
);
}
export default App;
Implementing Log Level Highlighting with Custom Cell Renderers
One of the most valuable features in a log viewer is the ability to visually distinguish log levels. We can achieve this using AG-Grid's Cell Renderers.
First, create a CSS file (e.g., src/App.css) for our custom styles:
/* src/App.css */
.log-level-INFO {
background-color: #e0ffe0; /* Light green */
color: #333;
}
.log-level-WARN {
background-color: #fffacd; /* Lemon Chiffon */
color: #b8860b; /* Goldenrod */
}
.log-level-ERROR {
background-color: #ffe0e0; /* Light red */
color: #dc3545; /* Bootstrap red */
font-weight: bold;
}
.log-level-DEBUG {
background-color: #e0e0ff; /* Light blue */
color: #6a5acd; /* Slate Blue */
}
/* Base styles for the grid */
.ag-theme-alpine {
--ag-border-color: #ddd;
--ag-header-background-color: #f5f5f5;
--ag-row-hover-color: #f7f7f7;
}
/* General styling for the app container */
body {
margin: 0;
font-family: Arial, sans-serif;
}
Remember to import this CSS file in your App.js: import './App.css';
Next, we'll create a functional component to act as our cell renderer for the 'level' column.
// src/components/LogLevelRenderer.js
import React from 'react';
const LogLevelRenderer = (props) => {
const level = props.value;
const className = `log-level-${level}`;
return (
<span className={className} style={{
display: 'inline-block',
width: '100%',
height: '100%',
lineHeight: '38px', /* Adjust based on row height */
paddingLeft: '5px',
}}>
{level}
</span>
);
};
export default LogLevelRenderer;
Now, integrate this renderer into your App.js:
// src/App.js (modifications)
// ... (imports and generateLogEntry function remain the same)
import './App.css'; // Don't forget to import your CSS
import LogLevelRenderer from './components/LogLevelRenderer'; // Import the renderer
function App() {
const gridRef = useRef();
const [rowData, setRowData] = useState([]);
const columnDefs = [
{ field: 'timestamp', headerName: 'Timestamp', width: 200, sortable: true, filter: 'agDateColumnFilter' },
{
field: 'level',
headerName: 'Level',
width: 100,
sortable: true,
filter: true,
cellRenderer: LogLevelRenderer, // <-- Use our custom renderer here
},
{ field: 'source', headerName: 'Source', width: 150, sortable: true, filter: true },
{ field: 'message', headerName: 'Message', flex: 1, sortable: true, filter: true },
];
// ... (rest of the App component remains the same)
}
export default App;
Filtering and Searching Logs
AG-Grid provides powerful filtering capabilities out of the box. For our log viewer, we want to allow users to filter by log level and search messages.
Built-in Column Filters
By setting filter: true in columnDefs, AG-Grid automatically provides text filters for string columns, date filters for date columns, etc. We've already enabled this for all our columns via defaultColDef.
The floatingFilter: true setting adds input fields directly below the column headers, making filtering very accessible.
Custom Log Level Filtering (Dropdown Filter)
While the default text filter works for 'level', a more intuitive approach for specific categories like log levels is a multi-select dropdown. AG-Grid allows you to use custom filter components or configure existing ones.
For 'level', we can use the built-in Set Filter, which provides checkbox-based filtering similar to Excel:
// src/App.js (modification in columnDefs)
const columnDefs = [
// ... other column definitions
{
field: 'level',
headerName: 'Level',
width: 100,
sortable: true,
filter: 'agSetColumnFilter', // <-- Use Set Filter
filterParams: {
values: ['INFO', 'WARN', 'ERROR', 'DEBUG'], // Provide distinct values
suppressApplyButton: true, // Apply filter immediately
},
cellRenderer: LogLevelRenderer,
},
// ... other column definitions
];
This modification will give users a friendly dropdown with checkboxes to select which log levels they want to see.
Real-time Log Updates
A crucial feature for any active log viewer is the ability to display new logs as they arrive. AG-Grid handles real-time updates efficiently using the Row Transaction API.
We'll simulate new log entries arriving every few seconds and add them to the grid.
// src/App.js (modifications)
// ... (imports and other code)
let nextLogId = 100; // Start ID after initial logs
function App() {
const gridRef = useRef();
const [rowData, setRowData] = useState([]);
// ... (columnDefs and defaultColDef remain the same)
useEffect(() => {
// Generate some initial logs
const initialLogs = Array.from({ length: 100 }, (_, i) => generateLogEntry(i));
setRowData(initialLogs);
nextLogId = 100; // Reset for subsequent additions
}, []);
const onGridReady = useCallback((params) => {
// Simulate real-time log additions
const intervalId = setInterval(() => {
const newLog = generateLogEntry(nextLogId++);
// Using gridRef.current.api.applyTransaction() for efficient updates
gridRef.current.api.applyTransaction({ add: [newLog] });
// Optional: Scroll to bottom if new logs arrive
// Be careful with auto-scrolling as it can be disruptive
// gridRef.current.api.ensureIndexVisible(rowData.length + 1, 'bottom');
}, 2000); // Add a new log every 2 seconds
return () => clearInterval(intervalId); // Cleanup interval on unmount
}, [rowData]); // rowData dependency means this effect re-runs if rowData changes,
// but applyTransaction directly updates the grid without state dependency for adding.
// Consider moving the interval creation outside useEffect if state updates cause issues,
// or ensure getRowId is stable.
// To make sure onGridReady does not depend on rowData in a way that restarts the interval:
// We need to ensure the interval is set up only once.
// The applyTransaction method works directly with the grid API, not the rowData state.
// So, removing rowData from the dependency array of onGridReady is often better for this pattern.
// If rowData state is truly needed, use a different approach or a mutable ref for rowData.
// Let's refine the onGridReady and useEffect for adding logs:
useEffect(() => {
const initialLogs = Array.from({ length: 100 }, (_, i) => generateLogEntry(i));
setRowData(initialLogs);
nextLogId = 100;
}, []); // Run once on mount
useEffect(() => {
const intervalId = setInterval(() => {
if (gridRef.current && gridRef.current.api) {
const newLog = generateLogEntry(nextLogId++);
gridRef.current.api.applyTransaction({ add: [newLog] });
// Optional: Auto-scroll to the bottom. Can be disruptive.
// gridRef.current.api.ensureIndexVisible(gridRef.current.api.getDisplayedRowCount() - 1, 'bottom');
}
}, 1500); // Add a new log every 1.5 seconds
return () => clearInterval(intervalId);
}, []); // Run once on mount for real-time updates
return (
Real-time Log Viewer
params.data.id, [])}
onGridReady={useCallback((params) => {
// This is where grid API is ready. We can perform actions if needed.
// But for continuous additions, the useEffect above is more suitable.
}, [])}
/>
);
}
export default App;
By using gridRef.current.api.applyTransaction({ add: [newLog] }), AG-Grid efficiently adds new rows without re-rendering the entire grid, maintaining performance even with rapid updates.
Customizing the Log Display
AG-Grid offers numerous ways to customize the display further:
- Row Styling: You can apply different styles to rows based on their data using
getRowStyleorgetRowClassgrid properties. For instance, you could make entire ERROR rows background red.// Example getRowClass in App.js const getRowClass = useCallback((params) => { return `log-row-${params.data.level}`; }, []); // Then in AgGridReact component: // <AgGridReact ... getRowClass={getRowClass} />And add corresponding CSS classes:
/* src/App.css */ .log-row-ERROR { background-color: #fce7e7 !important; /* Lighter red for the whole row */ } - Tooltips: For long messages that get truncated, tooltips can provide the full text. You can enable this globally with
tooltipShowDelayandtooltipHideDelayor via a customtooltipRendererfor specific columns. - Column Resizing/Reordering: These are enabled by default with
resizable: trueindefaultColDef. Users can drag column headers to resize or reorder. - Pivoting and Grouping: For advanced log analysis, AG-Grid Pro offers powerful pivoting and grouping features, allowing you to summarize logs by source, level, or time intervals.
Performance Considerations
When dealing with potentially massive log datasets, performance is paramount:
- Virtualization: AG-Grid's core strength is its row and column virtualization, meaning it only renders what's visible in the viewport. This is crucial for log viewers with thousands or millions of entries.
- Pagination: For extremely large, historical log archives, consider server-side pagination to load logs in chunks rather than all at once.
- Server-side Row Model: For truly enormous datasets where filtering, sorting, and grouping also need to happen on the server, AG-Grid's Server-side Row Model is the ideal solution. It delegates these operations to your backend, ensuring peak performance.
Conclusion
Building a sophisticated log viewer system with AG-Grid in React is not only feasible but highly effective. We've covered the basics of setting up the grid, enhancing readability with custom cell renderers for log levels, leveraging powerful filtering, and handling real-time data streams efficiently.
AG-Grid's flexibility and performance make it an excellent foundation for any data-intensive application, and a log viewer is a prime example. By combining these features, you can provide developers and operations teams with a powerful tool to monitor and debug their applications with unprecedented clarity and speed.