Live Events

Watch incoming events in your own dashboard in real time.

Live Events uses Server-Sent Events (SSE) to automatically stream new events as they arrive.

1. Create the API route

Create app/api/oneminutelogs/[type]/route.ts and paste this:

import { log } from "@/lib/logs";
import { handleProcessRequest } from "@oneminutelogs/next";

const forwardOneMinuteLogsRequest = (request: Request) =>
  handleProcessRequest({
    request,
    log,
  });

export const GET = forwardOneMinuteLogsRequest;
export const POST = forwardOneMinuteLogsRequest;

That's it.

2. Use it inside your dashboard

import { getStream } from "@oneminutelogs/next";

const { data: logs, isLoading } = getStream();

Every new event automatically appears inside logs.

3. Render your logs

Start with the smallest possible rendering loop.

{logs.map((log) => (
  <div key={log.id}>
    {log.message}
  </div>
))}

Production-ready dashboard

Want a complete dashboard?

Copy this Tailwind component when you want search, filtering, auto scrolling, status badges, relative timestamps, loading state, empty state, and responsive UI in one place.

Search
Filtering
Auto scrolling
Status badges
Relative timestamps
Loading and empty states

Install Tailwind CSS and shadcn/ui first. This full dashboard example uses shadcn/ui components like Input, Select, Button, and Tabs, so the code will not work until those dependencies are set up in your app.

"use client";

import React from "react";
import { Input } from "@/components/ui/input";
import {
  Select,
  SelectTrigger,
  SelectContent,
  SelectItem,
  SelectValue,
} from "@/components/ui/select";
import { Activity as ActivityIcon, Clipboard } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { getStream } from "@oneminutelogs/next";

// type definitions and helpers (added)
type LogLevel =
  | "info"
  | "warning"
  | "error"
  | "debug"
  | "success"
  | "audit"
  | "metric";
// Allow dynamic sources from stream (e.g., appName like "default")
type LogSource = string;

const levelColor: Record<LogLevel, string> = {
  info: "#60A5FA", // Blue
  warning: "#FBBF24", // Yellow/Amber
  error: "#F87171", // Red
  debug: "#A78BFA", // Purple
  success: "#34D399", // Emerald/Green
  audit: "#34D399", // Emerald/Green
  metric: "#22D3EE", // Cyan
};

type LogEntry = {
  id: string;
  ts: string;
  level: LogLevel;
  source: LogSource;
  message: string;
  payload: Record<string, unknown>;
};

export default function LiveLogsPage() {
  const { data: logs, isLoading } = getStream();
  const [autoScroll, setAutoScroll] = React.useState(true);
  const [filterLevel, setFilterLevel] = React.useState<"All" | LogLevel>("All");
  const [search, setSearch] = React.useState("");
  const [selected, setSelected] = React.useState<LogEntry | null>(null);
  const streamRef = React.useRef<HTMLDivElement>(null);

  React.useEffect(() => {
    if (!autoScroll) return;
    streamRef.current?.scrollTo({ top: streamRef.current.scrollHeight });
  }, [logs, autoScroll]);

  const filteredLogs: LogEntry[] = React.useMemo(() => {
    return logs.filter((l) => {
      const byLevel = filterLevel === "All" ? true : l.level === filterLevel;
      const bySearch =
        search.trim().length === 0
          ? true
          : \`\${l.message} \${JSON.stringify(l.payload)}\`
              .toLowerCase()
              .includes(search.toLowerCase());
      return byLevel && bySearch;
    });
  }, [logs, filterLevel, search]);

  const stats = React.useMemo(() => {
    const total = filteredLogs.length;
    let errors = 0;
    let warnings = 0;
    let success = 0;
    for (const l of filteredLogs) {
      if (l.level === "error") errors++;
      else if (l.level === "warning") warnings++;
      else if (l.level === "success") success++;
    }
    return { total, errors, warnings, success };
  }, [filteredLogs]);

  if (isLoading) {
    return null;
  }

  return (
    <div className="space-y-4">
      {/* Header */}
      <div className="flex items-start justify-between">
        <div>
          <h1 className="text-2xl font-semibold tracking-tight">Event Tail</h1>
          <p className="text-sm text-muted-foreground">
            Watch new event lines arrive every second.
          </p>
        </div>
      </div>

      {/* Filters Row (horizontal) */}
      <div className="flex items-end gap-3">
        <div className="w-55">
          <div className="text-xs text-muted-foreground mb-1">Category</div>
          <Select
            value={filterLevel}
            onValueChange={(v) =>
              setFilterLevel(v === "All" ? "All" : (v as LogLevel))
            }
          >
            <SelectTrigger className="w-full rounded-xl">
              <SelectValue placeholder="Select category" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="All">All</SelectItem>
              <SelectItem value="error">Error</SelectItem>
              <SelectItem value="warning">Warning</SelectItem>
              <SelectItem value="info">Info</SelectItem>
              <SelectItem value="debug">Debug</SelectItem>
              <SelectItem value="success">Success</SelectItem>
              <SelectItem value="audit">Audit</SelectItem>
              <SelectItem value="metric">Metric</SelectItem>
            </SelectContent>
          </Select>
        </div>
        <div className="flex-1">
          <div className="text-xs text-muted-foreground mb-1">Search</div>
          <Input
            placeholder="Search events (e.g. message: timeout)"
            value={search}
            onChange={(e) => setSearch(e.target.value)}
            className="rounded-xl"
          />
        </div>
      </div>

      {/* Log Viewer */}
      <div className="space-y-2">
        <div className="mb-2 flex items-center justify-between">
          <div className="flex items-center gap-2">
            <ActivityIcon className="h-4 w-4 opacity-60" />
            <span className="text-xs text-muted-foreground">
              Live tail • {logs.length} entries
            </span>
          </div>
          <label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer select-none">
            <input
              type="checkbox"
              checked={autoScroll}
              onChange={(e) => setAutoScroll(e.target.checked)}
            />
            Auto-scroll
          </label>
        </div>

        <div
          ref={streamRef}
          className="h-[60vh] w-full overflow-y-auto rounded-xl border"
          style={{
            border: "1px solid rgba(255,255,255,0.05)",
            background: "#0B0F13",
            fontFamily: "JetBrains Mono, monospace",
            fontSize: "13px",
            lineHeight: "1.6",
          }}
        >
          {filteredLogs.length === 0 ? (
            <div className="flex h-full items-center justify-center text-muted-foreground">
              <ActivityIcon className="mr-2 h-4 w-4" />
              No event available yet!
            </div>
          ) : (
            <ul className="divide-y divide-white/5">
              {filteredLogs.map((l) => (
                <li
                  key={l.id}
                  className="group px-3 py-2 cursor-pointer hover:bg-white/2 transition-colors"
                  onClick={() => setSelected(l)}
                >
                  <span className="text-[11px] text-white/50 mr-2">
                    {new Date(l.ts).toLocaleTimeString()}
                  </span>
                  <span
                    className="mr-2 font-medium"
                    style={{ color: levelColor[l.level] }}
                  >
                    [{l.level.toUpperCase()}]
                  </span>
                  <span className="mr-2 text-white/70">{l.source}</span>
                  <span className="block text-white/90 whitespace-nowrap overflow-hidden text-ellipsis group-hover:whitespace-normal group-hover:wrap-break-word">
                    {l.message}
                  </span>
                </li>
              ))}
            </ul>
          )}
        </div>
      </div>

      {selected && (
        <div
          className="fixed inset-0 z-50 grid place-items-center bg-black/50"
          onClick={() => setSelected(null)}
        >
          <div
            className="w-105 rounded-xl border bg-background p-4"
            style={{ background: "#0E1117" }}
            onClick={(e) => e.stopPropagation()}
          >
            <div className="flex items-center justify-between">
              <h3 className="text-sm font-medium">Log Details</h3>
              <Button
                variant="outline"
                size="sm"
                className="rounded-xl hover:bg-white/2"
                onClick={() => {
                  navigator.clipboard.writeText(
                    JSON.stringify(selected, null, 2),
                  );
                }}
              >
                <Clipboard className="mr-2 h-3.5 w-3.5" />
                Copy JSON
              </Button>
            </div>
            <Tabs defaultValue="formatted" className="mt-3">
              <TabsList>
                <TabsTrigger value="formatted">Formatted</TabsTrigger>
                <TabsTrigger value="raw">Raw JSON</TabsTrigger>
              </TabsList>
              <TabsContent value="formatted" className="mt-3">
                <div className="space-y-2 text-sm">
                  <div className="flex justify-between">
                    <span className="text-muted-foreground">Timestamp</span>
                    <span>{new Date(selected.ts).toLocaleString()}</span>
                  </div>
                  <div className="flex justify-between">
                    <span className="text-muted-foreground">Level</span>
                    <span
                      className="font-medium"
                      style={{ color: levelColor[selected.level] }}
                    >
                      {selected.level}
                    </span>
                  </div>
                  <div className="flex justify-between">
                    <span className="text-muted-foreground">Source</span>
                    <span>{selected.source}</span>
                  </div>
                  <div>
                    <div className="text-muted-foreground mb-1">Message</div>
                    <div className="rounded-lg border border-white/10 bg-black/20 p-2">
                      {selected.message}
                    </div>
                  </div>
                  <div>
                    <div className="text-muted-foreground mb-1">Payload</div>
                    <pre className="rounded-lg border border-white/10 bg-black/20 p-2 overflow-x-auto text-xs">
                      {JSON.stringify(selected.payload, null, 2)}
                    </pre>
                  </div>
                </div>
              </TabsContent>
              <TabsContent value="raw" className="mt-3">
                <pre className="rounded-lg border border-white/10 bg-black/20 p-2 overflow-x-auto text-xs">
                  {JSON.stringify(selected, null, 2)}
                </pre>
              </TabsContent>
            </Tabs>
          </div>
        </div>
      )}

      <div
        className="flex items-center justify-between rounded-xl border px-3 py-2 text-xs"
        style={{
          background: "rgba(255,255,255,0.02)",
          border: "1px solid rgba(255,255,255,0.05)",
        }}
      >
        <div className="flex items-center gap-6">
          <span className="text-muted-foreground">Events Loaded</span>
          <span className="font-medium">{stats.total.toLocaleString()}</span>
          <span className="text-muted-foreground">Errors</span>
          <span className="font-medium">{stats.errors}</span>
          <span className="text-muted-foreground">Warnings</span>
          <span className="font-medium">{stats.warnings}</span>
        </div>
      </div>
    </div>
  );
}

Common mistakes

  • Create the API route exactly as shown.
  • Initialize the logger before using getStream().
  • Do not modify the request handler.
Found something unexpected? Let us know.