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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 0 additions & 136 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,137 +1 @@
# Logs Explorer Template

This is a template for a Logs Explorer web application. It is built with Next.js and [Tinybird](https://tinybird.co).

Use this template to bootstrap a multi-tenant, user-facing logs explorer for any software project. Fork it and make it your own!

## Quick Start

Deploy the Tinybird and Next.js to the cloud to get started quickly.

<p align="left">
<a href="https://app.tinybird.co?starter_kit=https://github.com/tinybirdco/logs-explorer-template/tinybird">
<img width="200" src="https://img.shields.io/badge/Deploy%20to-Tinybird-25283d?style=flat&labelColor=25283d&color=27f795&logo=data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAwIDUwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNTAwIDQyLjhsLTE1Ni4xLTQyLjgtNTQuOSAxMjIuN3pNMzUwLjcgMzQ1LjRsLTE0Mi45LTUxLjEtODMuOSAyMDUuN3oiIGZpbGw9IiNmZmYiIG9wYWNpdHk9Ii42Ii8+PHBhdGggZD0iTTAgMjE5LjlsMzUwLjcgMTI1LjUgNTcuNS0yNjguMnoiIGZpbGw9IiNmZmYiLz48L3N2Zz4=" />
</a>
</p>

[![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Ftinybirdco%2Flogs-explorer-template&project-name=tinybird-logs-explorer-template&repository-name=tinybird-logs-explorer-template&demo-description=Custom%20logs%20explorer%20for%20your%20application%20logs%20using%20Tinybird&demo-url=http%3A%2F%2Flogs.tinybird.app&demo-image=//github.com/tinybirdco/logs-explorer-template/blob/main/dashboard/log-analyzer/public/banner.png?raw=true&root-directory=dashboard/log-analyzer)

Append the `tinybird/fixtures/logs.ndjson` file to the `logs` Data Source or stream some mock data.

Configure Environment Variables and you are ready to go:

```bash
NEXT_PUBLIC_TINYBIRD_API_KEY=<YOUR_TINYBIRD_ADMIN_TOKEN>
NEXT_PUBLIC_TINYBIRD_API_URL=<YOUR_TINYBIRD_REGION_HOST>
```

## Local Development

Get started by forking the [GitHub repository](https://github.com/tinybirdco/logs-explorer-template) and then customizing it to your needs.

Start Tinybird locally:

```bash
curl -LsSf https://tbrd.co/fwd | sh
cd tinybird
tb local start
tb login
tb dev
token ls # copy an admin token
```

Configure the Next.js application:

```bash
cd dashboard/log-analyzer
cp .env.example .env
```

Edit the `.env` file with your Tinybird API key and other configuration.

```bash
NEXT_PUBLIC_TINYBIRD_API_KEY=<YOUR_TINYBIRD_ADMIN_TOKEN>
NEXT_PUBLIC_TINYBIRD_API_URL=http://localhost:7181
```

Start the Next.js application:

```bash
cd dashboard/log-analyzer
npm install
npm run dev
```

Open the application in your browser:

```bash
http://localhost:3000
```

Read the [dashboard/log-analyzer/README.md](./dashboard/log-analyzer/README.md) file for more information on how to use the application and [tinybird/README.md](./tinybird/README.md) for more information on how to customize the template.

## Instrumenting your application

To instrument your application, just send JSON objects to the Tinybird [Events API](https://www.tinybird.co/docs/get-data-in/ingest-apis/events-api).

```typescript
const data = {
timestamp: new Date().toISOString(),
level: 'info',
service: 'my-app',
message: 'This is a test message',
request_id: '1234567890',
environment: 'development',
status_code: 200,
response_time: 100,
request_method: 'GET',
request_path: '/',
host: 'my-app.com',
user_agent: req.headers.get('user-agent')
}
await fetch(
`https://<YOUR_TINYBIRD_HOST>/v0/events?name=logs`,
{
method: 'POST',
body: JSON.stringify(data),
headers: { Authorization: `Bearer ${process.env.TINYBIRD_APPEND_TOKEN}` },
}
)
```

The example above uses the [logs](./tinybird/datasources/logs.datasource) Data Source and schema in this template but you can use your own Data Source and schema, append logs and build your own logs explorer application.

Check the [examples](./examples) folder for some examples of how to do this with different languages, services and schemas.

## Building a log aggregator with Vector

Vector is a log aggregator that is used to collect, process, and store logs built by DataDog.

You can use Vector to collect logs from different sources and send them to a Tinybird Sink.

Check the [examples/vector](./examples/vector) folder for an example of how to do this with Vector.

## Deployment

Deploy the Tinybird project to the cloud:

```bash
cd tinybird
tb --cloud deploy
```

Once deployed copy your Tinybird cloud host and `read_pipes` token, [deploy the Next.js application to Vercel](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Ftinybirdco%2Flogs-explorer-template&project-name=tinybird-logs-explorer-template&repository-name=tinybird-logs-explorer-template&demo-description=Custom%20logs%20explorer%20for%20your%20application%20logs%20using%20Tinybird&demo-url=http%3A%2F%2Flogs.tinybird.app&demo-image=//github.com/tinybirdco/logs-explorer-template/blob/main/dashboard/log-analyzer/public/banner.png?raw=true&root-directory=dashboard/log-analyzer) and configure the environment variables.

## Contributing

Please open an issue or submit a pull request.

## Support

Join the Tinybird [Slack community](https://www.tinybird.co/community) to get help with your project.

## License

MIT License

Copyright (c) 2025 Tinybird.co
3 changes: 1 addition & 2 deletions dashboard/log-analyzer/src/components/filters/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,12 @@ export function SearchBar() {
const params = {
start_date: searchParams.get('start_date') || undefined,
end_date: searchParams.get('end_date') || undefined,
service: searchParams.get('service')?.split(',').filter(Boolean) || undefined,
project_name: searchParams.get('project_name')?.split(',').filter(Boolean) || undefined,
level: searchParams.get('level')?.split(',').filter(Boolean) || undefined,
environment: searchParams.get('environment')?.split(',').filter(Boolean) || undefined,
request_method: searchParams.get('request_method')?.split(',').filter(Boolean) || undefined,
status_code: searchParams.get('status_code')?.split(',').filter(Boolean)?.map(Number) || undefined,
request_path: searchParams.get('request_path')?.split(',').filter(Boolean) || undefined,
user_agent: searchParams.get('user_agent')?.split(',').filter(Boolean) || undefined,
};

const totalCount = await getTotalRowCount(params);
Expand Down
6 changes: 3 additions & 3 deletions dashboard/log-analyzer/src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ export default function Sidebar() {

{/* Services */}
<GenericCounter
columnName="service"
title="Services"
onSelectionChange={createFilterHandler('service')}
columnName="projectName"
title="Project"
onSelectionChange={createFilterHandler('projectName')}
shouldRefresh={refreshTrigger}
startOpen={true}
/>
Expand Down
24 changes: 2 additions & 22 deletions dashboard/log-analyzer/src/components/logs/LogDetailPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,6 @@ export function LogDetailPanel({ log, onClose, isOpen }: LogDetailPanelProps) {
});
};

const formatTime = (ms: number) => {
return `${ms}ms`;
};

return (
<div className={cn(
"fixed top-0 right-0 h-screen w-[600px] bg-[--background-secondary] z-50",
Expand Down Expand Up @@ -120,12 +116,6 @@ export function LogDetailPanel({ log, onClose, isOpen }: LogDetailPanelProps) {
<span className="text-sm text-[--text-secondary]">Host</span>
<span className="text-sm text-[--text-secondary] truncate max-w-[400px]">app.tinybird.co</span>
</div>

{/* User Agent */}
<div className="flex justify-between items-center">
<span className="text-sm text-[--text-secondary]">User Agent</span>
<span className="text-sm text-[--text-secondary] truncate max-w-[400px]">{log.user_agent}</span>
</div>
</div>
</div>

Expand All @@ -145,19 +135,9 @@ export function LogDetailPanel({ log, onClose, isOpen }: LogDetailPanelProps) {
<span className="text-sm text-[--text-secondary] truncate max-w-[400px]">{log.environment}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-[--text-secondary]">Service</span>
<span className="text-sm text-[--text-secondary] truncate max-w-[400px]">{log.service}</span>
</div>
</div>
</div>

<div className="px-8 mt-6 text-white">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded-full bg-[#27F795] border-2 border-white"></div>
<span>Request finished</span>
<span className="text-sm text-[--text-secondary]">Project</span>
<span className="text-sm text-[--text-secondary] truncate max-w-[400px]">{log.projectName}</span>
</div>
<span>{formatTime(log.response_time)}</span>
</div>
</div>
</div>
Expand Down
12 changes: 6 additions & 6 deletions dashboard/log-analyzer/src/components/logs/LogTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function LogTable({ logs = [], onSort, sortColumn, sortOrder, observerRef
Time{renderSortIndicator('timestamp')}
</TableHead>
<TableHead className="w-[8%] whitespace-nowrap px-4 py-3 text-left">Level</TableHead>
<TableHead className="w-[12%] whitespace-nowrap px-4 py-3 text-left">Service</TableHead>
<TableHead className="w-[12%] whitespace-nowrap px-4 py-3 text-left">Project</TableHead>
<TableHead className="w-[8%] whitespace-nowrap px-4 py-3 text-left">Method</TableHead>
<TableHead className="w-[8%] whitespace-nowrap px-4 py-3 text-left">Status</TableHead>
<TableHead className="w-[14%] px-4 py-3 text-left">Path</TableHead>
Expand Down Expand Up @@ -112,16 +112,16 @@ export function LogTable({ logs = [], onSort, sortColumn, sortOrder, observerRef
</TableCell>
<TableCell className="w-[8%] whitespace-nowrap px-4 py-3 text-left">
<span className={`inline-flex items-center rounded-sm px-2 py-1 text-xs font-medium
${log.level === 'ERROR' ? 'bg-[var(--bg-pill-error)] text-[var(--text-pill-error)]' :
log.level === 'WARN' ? 'bg-[var(--bg-pill-warn)] text-[var(--text-pill-warn)]' :
log.level === 'INFO' ? 'bg-[var(--bg-pill-info)] text-[var(--text-pill-info)]' :
log.level === 'DEBUG' ? 'bg-[var(--bg-pill-debug)] text-[var(--text-pill-debug)]' :
${log.level === 'error' ? 'bg-[var(--bg-pill-error)] text-[var(--text-pill-error)]' :
log.level === 'warning' ? 'bg-[var(--bg-pill-warn)] text-[var(--text-pill-warn)]' :
log.level === 'info' ? 'bg-[var(--bg-pill-info)] text-[var(--text-pill-info)]' :
log.level === 'debug' ? 'bg-[var(--bg-pill-debug)] text-[var(--text-pill-debug)]' :
'bg-[var(--bg-pill-default)] text-[var(--text-pill-default)]'}`
}>
{log.level}
</span>
</TableCell>
<TableCell className="w-[12%] whitespace-nowrap px-4 py-3 text-left">{log.service}</TableCell>
<TableCell className="w-[12%] whitespace-nowrap px-4 py-3 text-left">{log.projectName}</TableCell>
<TableCell className="w-[8%] whitespace-nowrap px-4 py-3 text-left">{log.request_method}</TableCell>
<TableCell className="w-[8%] whitespace-nowrap px-4 py-3 text-left">
<span className={`inline-flex items-center rounded-sm px-2 py-1 text-xs font-medium
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,12 @@ export function LogTableWithPagination({ pageSize }: LogTableWithPaginationProps
const filters = {
start_date: searchParams.get('start_date') || undefined,
end_date: searchParams.get('end_date') || undefined,
service: searchParams.get('service')?.split(',').filter(Boolean) || undefined,
projectName: searchParams.get('projectName')?.split(',').filter(Boolean) || undefined,
level: searchParams.get('level')?.split(',').filter(Boolean) || undefined,
environment: searchParams.get('environment')?.split(',').filter(Boolean) || undefined,
request_method: searchParams.get('request_method')?.split(',').filter(Boolean) || undefined,
status_code: searchParams.get('status_code')?.split(',').filter(Boolean)?.map(Number) || undefined,
request_path: searchParams.get('request_path')?.split(',').filter(Boolean) || undefined,
user_agent: searchParams.get('user_agent')?.split(',').filter(Boolean) || undefined,
message: searchParams.get('message') || undefined,
sort_by: currentSortColumn || undefined,
order: currentSortOrder || undefined,
Expand Down Expand Up @@ -65,7 +64,7 @@ export function LogTableWithPagination({ pageSize }: LogTableWithPaginationProps
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const shouldUseExplorerApi = useCallback(async (filters: Record<string, any>) => {
// First check if only allowed filters are being used
const allowedFilters = ['start_date', 'end_date', 'environment', 'service', 'level', 'order', 'sort_by'];
const allowedFilters = ['start_date', 'end_date', 'environment', 'projectName', 'level', 'order', 'sort_by'];
const activeFilters = Object.keys(filters);
const onlyAllowedFilters = activeFilters.every(filter => allowedFilters.includes(filter));

Expand Down
12 changes: 3 additions & 9 deletions dashboard/log-analyzer/src/components/metrics/GenericCounter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,6 @@ const formatNumber = (num: number) => {
return num.toString();
};

const capitalizeFirstLetter = (str: string) => {
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
};

export function GenericCounter({
columnName,
title,
Expand All @@ -59,13 +55,12 @@ export function GenericCounter({
const [isOpen, setIsOpen] = useState(startOpen);
const initializedRef = useRef(false);
const currentParams = searchParams.get(columnName);
const service = searchParams.get('service')?.split(',').filter(Boolean) || undefined;
const projectName = searchParams.get('projectName')?.split(',').filter(Boolean) || undefined;
const level = searchParams.get('level')?.split(',').filter(Boolean) || undefined;
const environment = searchParams.get('environment')?.split(',').filter(Boolean) || undefined;
const requestMethod = searchParams.get('request_method')?.split(',').filter(Boolean) || undefined;
const statusCode = searchParams.get('status_code')?.split(',').filter(Boolean)?.map(Number) || undefined;
const requestPath = searchParams.get('request_path')?.split(',').filter(Boolean) || undefined;
const userAgent = searchParams.get('user_agent')?.split(',').filter(Boolean) || undefined;

useDefaultDateRange();

Expand All @@ -92,13 +87,12 @@ export function GenericCounter({
column_name: columnName,
start_date: start_date || format(defaultStartDate, 'yyyy-MM-dd HH:mm:ss'),
end_date: end_date || format(defaultEndDate, 'yyyy-MM-dd HH:mm:ss'),
service: columnName !== 'service' ? service : undefined,
projectName: columnName !== 'projectName' ? projectName : undefined,
level: columnName !== 'level' ? level : undefined,
environment: columnName !== 'environment' ? environment : undefined,
request_method: columnName !== 'request_method' ? requestMethod : undefined,
status_code: columnName !== 'status_code' ? statusCode : undefined,
request_path: columnName !== 'request_path' ? requestPath : undefined,
user_agent: columnName !== 'user_agent' ? userAgent : undefined,
};

const response = await genericCounterApi(params);
Expand Down Expand Up @@ -253,7 +247,7 @@ export function GenericCounter({
}}
/>
<span className="cursor-pointer max-w-[150px] truncate">
{capitalizeFirstLetter(category)}
{category}
</span>
</div>
<span className="text-[12px] leading-[16px] text-text-primary bg-[var(--gray-100)] rounded-sm px-1 font-medium h-5 flex items-center justify-center min-w-[20px] tracking-[1px]">
Expand Down
9 changes: 3 additions & 6 deletions dashboard/log-analyzer/src/lib/tinybird.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@ export const logAnalysisApi = tb.buildPipe({
page_size: z.number(),
start_date: z.string().optional(),
end_date: z.string().optional(),
service: z.array(z.string()).optional(),
projectName: z.array(z.string()).optional(),
level: z.array(z.string()).optional(),
environment: z.array(z.string()).optional(),
request_method: z.array(z.string()).optional(),
status_code: z.array(z.number()).optional(),
request_path: z.array(z.string()).optional(),
user_agent: z.array(z.string()).optional(),
message: z.string().optional(),
sort_by: z.string().optional(),
order: z.string().optional(),
Expand All @@ -32,13 +31,12 @@ export const logExplorerApi = tb.buildPipe({
page_size: z.number(),
start_date: z.string().optional(),
end_date: z.string().optional(),
service: z.array(z.string()).optional(),
projectName: z.array(z.string()).optional(),
level: z.array(z.string()).optional(),
environment: z.array(z.string()).optional(),
request_method: z.array(z.string()).optional(),
status_code: z.array(z.number()).optional(),
request_path: z.array(z.string()).optional(),
user_agent: z.array(z.string()).optional(),
message: z.string().optional(),
sort_by: z.string().optional(),
order: z.string().optional(),
Expand All @@ -52,13 +50,12 @@ export const genericCounterApi = tb.buildPipe({
column_name: z.string(),
start_date: z.string().optional(),
end_date: z.string().optional(),
service: z.array(z.string()).optional(),
projectName: z.array(z.string()).optional(),
level: z.array(z.string()).optional(),
environment: z.array(z.string()).optional(),
request_method: z.array(z.string()).optional(),
status_code: z.array(z.number()).optional(),
request_path: z.array(z.string()).optional(),
user_agent: z.array(z.string()).optional(),
__tb__deployment: z.string().optional(),
}),
data: z.object({
Expand Down
4 changes: 1 addition & 3 deletions dashboard/log-analyzer/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ export const LogEntrySchema = z.object({
timestamp: z.string(),
request_method: z.string(),
status_code: z.number(),
service: z.string(),
projectName: z.string(),
request_path: z.string(),
level: z.string(),
message: z.string(),
user_agent: z.string(),
response_time: z.number(),
environment: z.string(),
});

Expand Down
3 changes: 1 addition & 2 deletions dashboard/log-analyzer/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ export function cn(...inputs: ClassValue[]) {
export async function getTotalRowCount(params: {
start_date?: string;
end_date?: string;
service?: string[];
project_name?: string[];
level?: string[];
environment?: string[];
request_method?: string[];
status_code?: number[];
request_path?: string[];
user_agent?: string[];
}) {
try {
const response = await genericCounterApi({
Expand Down
Loading