Skip to main content

Log Viewer

Logs are a crucial component of any software application, offering valuable insights into its performance, errors, and user interactions, so you often need to expose them to your users and/or application administrators.

ElectricSQL provides a simple and efficient way to view logs in your application, with the ability to filter, sort, and paginate them efficiently while also receiving them live as they are being created.

This recipe demonstrates how to build a log viewer for arbitrary, unstructured text logs, such as web-server logs.

Schema

Adapt the schema and DDLX commands below to match your specific use-case.

-- Create a simple logs table.
CREATE TABLE IF NOT EXISTS logs (
id UUID PRIMARY KEY NOT NULL,
-- can be a foreign key to a source table, to refine
-- access to logs and who can view them
source_id UUID NOT NULL,
timestamp TIMESTAMPTZ NOT NULL,
content TEXT NOT NULL
);

-- Index for timestamp column
CREATE INDEX logs_idx_timestamp ON logs(timestamp);

-- ⚡ Electrify the table
ALTER TABLE logs ENABLE ELECTRIC;

Data Access

Adapt the headless components below and enhance them with additional features.

import { useElectric } from '../electric/ElectricWrapper'
import { useLiveQuery } from 'electric-sql/react'

export const useLogs = ({
maxNumberOfLogs = 10,
searchFilter = '',
sourceId,
}: {
maxNumberOfLogs: number
searchFilter: string
sourceId?: string
}) => {
const { db } = useElectric()!

// Retrieve specified number of logs matching filter in descending
// chronological order
const { results: logs = [] } = useLiveQuery(
db.logs.liveMany({
where: {
content: { contains: searchFilter },
...(sourceId && { source_id: sourceId }),
},
orderBy: { timestamp: 'desc' },
take: maxNumberOfLogs,
}),
)

// Use raw SQL to count all logs matching filter
const totalNumberOfLogs =
useLiveQuery(
db.liveRawQuery({
sql: `
SELECT COUNT(*) AS count FROM logs WHERE
content LIKE ?
${sourceId ? `AND source_id = ?` : ''}
`,
args: [`%${searchFilter}%`, ...(sourceId ? [sourceId] : [])],
}),
).results?.[0]?.count ?? 0

return {
logs,
totalNumberOfLogs,
}
}

Usage

Connect the schema and headless components with your UI library of choice to get a working component.

import { useCallback, useEffect, useState } from 'react'
import { LogViewerView } from './LogViewerView'
import { useLogs } from './use_logs'

export const LogViewer = ({
defaultNumLogsToShow = 10,
additionalLogsToShow = 10,
}: {
defaultNumLogsToShow?: number
additionalLogsToShow?: number
}) => {
const [numLogsToShow, setNumLogsToShow] = useState(defaultNumLogsToShow)
const [searchFilter, setSearchFilter] = useState('')

const { logs, totalNumberOfLogs } = useLogs({
maxNumberOfLogs: numLogsToShow,
searchFilter,
})

// Reset number of logs shown when updating search filter
useEffect(() => {
if (searchFilter.length > 0) setNumLogsToShow(defaultNumLogsToShow)
}, [searchFilter, defaultNumLogsToShow])

const handleShowMore = useCallback(
() => setNumLogsToShow((currentNum) => currentNum + additionalLogsToShow),
[additionalLogsToShow],
)

return (
<LogViewerView
logs={logs}
numHiddenLogs={totalNumberOfLogs - numLogsToShow}
onSearchFilterChange={setSearchFilter}
onShowMoreLogs={handleShowMore}
/>
)
}