Search & Filters

Search, filter, and inspect stored events.

Search & Filters uses the same OneMinute Logs API route you created for Live Events. No new backend endpoint is required.

Same API endpoint

If you already created the Live Events route, search and filters work through that same file. The SDK calls /api/oneminutelogs/logs for stored events and /api/oneminutelogs/stream for live events.

// Already created for Live Events
app/api/oneminutelogs/[type]/route.ts

Use getLogs()

Pass filters to getLogs() and render the returned rows in your dashboard.

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

const { data, isLoading, error } = getLogs({
  type: "error",
  appName: "billing",
  search: "timeout",
  limit: 200,
});

Query syntax

The dashboard example accepts human-friendly query strings and turns them into supported filters like type, appName, env, and search.

type:error
appName:api AND search:timeout
type:warning AND appName:billing
env:production AND type:error

Render results

Start with a simple table row, then add filters, details, and copy actions as your dashboard grows.

{results.map((event) => (
  <tr key={event.keyId}>
    <td>{event.timestamp}</td>
    <td>{event.type}</td>
    <td>{event.appName}</td>
    <td>{event.message}</td>
  </tr>
))}

Complete dashboard

Copy the full Search & Filters UI.

This example follows the main dashboard query page: query bar, examples, results table, loading state, empty state, details drawer, raw JSON view, and footer stats.

Query examples
Filter parsing
Results table
Details drawer
Loading state
Stored events

Install Tailwind CSS and shadcn/ui first. This example uses shadcn/ui components like Input, Button, Tabs, and Table.

"use client";
import React from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import {
  Table,
  TableHeader,
  TableHead,
  TableRow,
  TableBody,
  TableCell,
} from "@/components/ui/table";
import { Play, XCircle, Search, Clipboard, Loader2 } from "lucide-react";
import { getLogs } from "@oneminutelogs/next";

type ResultRow = {
  timestamp?: string;
  type?: "error" | "warning" | "info" | "audit" | "metric";
  appName?: string;
  message?: string;
  environment?: string;
  importance?: number | string | null;
  subsystem?: string | null;
  operation?: string | null;
  [key: string]: any;
};

export default function QueriesPage() {
  const [filters, setFilters] = React.useState<{
    limit?: number;
    query?: string;
    type?: string;
    env?: string;
    appName?: string;
    search?: string;
  }>({});
  const { data, isLoading, error } = getLogs(filters);

  const levelColor: Record<string, string> = {
    info: "#60A5FA",
    warning: "#FBBF24",
    error: "#F87171",
    debug: "#A78BFA",
    success: "#34D399",
    audit: "#34D399",
    metric: "#22D3EE",
  };

  const [query, setQuery] = React.useState("");
  const [selected, setSelected] = React.useState<ResultRow | null>(null);

  const examples = [
    "type:error",
    "appName:api AND search:timeout",
    "type:warning AND appName:billing",
  ];

  const normalizeLogs = React.useCallback((input: any): ResultRow[] => {
    if (!input) return [];
    if (Array.isArray(input)) return input as ResultRow[];
    const obj = input as { logs?: any };
    if (Array.isArray(obj.logs)) return obj.logs as ResultRow[];
    return [];
  }, []);

  const normalizeQuery = (q: string) =>
    q
      .replace(/\blevel:/gi, "type:")
      .replace(/\bservice:/gi, "appName:")
      .replace(/\bmessage:/gi, "search:")
      .trim();

  const parseQueryFilters = React.useCallback((q: string) => {
    const out: {
      type?: string;
      env?: string;
      appName?: string;
      search?: string;
    } = {};
    const parts = q.trim().split(/\s+AND\s+|\s+/i);
    for (const p of parts) {
      const [rawKey, ...rest] = p.split(":");
      if (!rawKey || rest.length === 0) continue;
      const value = rest.join(":").trim();
      const key = rawKey.trim().toLowerCase();
      const mapped =
        key === "level"
          ? "type"
          : key === "service"
            ? "appName"
            : key === "message"
              ? "search"
              : key;

      if (mapped === "type") out.type = value;
      else if (mapped === "env" || mapped === "environment") out.env = value;
      else if (mapped === "appname" || mapped === "app") out.appName = value;
      else if (mapped === "search") out.search = value;
    }
    return out;
  }, []);

  const results: ResultRow[] = React.useMemo(() => {
    const rows = normalizeLogs(data);
    return rows ?? [];
  }, [data, normalizeLogs]);

  const runQuery = () => {
    const parsed = parseQueryFilters(normalizeQuery(query));
    setFilters((prev) => ({
      ...prev,
      ...parsed,
      limit: 200,
    }));
  };

  const clearQuery = () => {
    setQuery("");
    setFilters({ limit: 100 });
  };

  const stats = React.useMemo(() => {
    const total = results.length;
    const errors = results.filter(
      (r) => String(r.type || "").toLowerCase() === "error",
    ).length;
    const warnings = results.filter((r) => {
      const t = String(r.type || "").toLowerCase();
      return t === "warning" || t === "warn";
    }).length;
    return { total, errors, warnings };
  }, [results]);

  return (
    <div className="space-y-4">
      <div>
        <h1 className="text-2xl font-semibold tracking-tight">Queries</h1>
        <p className="text-sm text-muted-foreground">
          Search, filter, and analyze events stored in your project.
        </p>
      </div>

      <div className="rounded-xl border p-3">
        <div className="flex gap-3">
          <Input
            placeholder="Enter query (e.g. type:error AND appName:billing)"
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            onKeyDown={(e) => {
              if (e.key === "Enter") runQuery();
            }}
            className="rounded-xl flex-1"
          />
          <Button onClick={runQuery} className="rounded-xl">
            <Play className="h-4 w-4" />
            Run Query
          </Button>
          <Button
            variant="ghost"
            onClick={clearQuery}
            className="rounded-xl hover:bg-white/2"
          >
            <XCircle className="h-4 w-4" />
            Clear
          </Button>
        </div>
        <div className="mt-2 flex items-center gap-2">
          <Search className="h-4 w-4 opacity-60" />
          <div className="text-xs text-muted-foreground">Examples:</div>
          <div className="flex flex-wrap gap-2">
            {examples.map((ex) => (
              <button
                key={ex}
                onClick={() => setQuery(ex)}
                className="text-xs rounded-full border px-2 py-1 text-muted-foreground hover:bg-white/3"
              >
                {ex}
              </button>
            ))}
          </div>
        </div>
      </div>

      <div className="rounded-xl border">
        <div className="flex items-center justify-between px-3 py-2">
          <h3 className="text-sm font-medium">Query Results</h3>
          <span className="text-xs text-muted-foreground flex items-center gap-2">
            {isLoading && <Loader2 className="h-3 w-3 animate-spin" />}
            {stats.total} rows
          </span>
        </div>
        <div
          style={{
            fontFamily: "JetBrains Mono, monospace",
            fontSize: "13px",
          }}
        >
          {error ? (
            <div className="flex items-center justify-center py-12 text-red-400">
              Failed to load events: {String(error)}
            </div>
          ) : results.length === 0 ? (
            <div className="flex items-center justify-center py-12 text-muted-foreground">
              <Search className="mr-2 h-4 w-4" />
              No events found.
            </div>
          ) : (
            <Table className="min-w-full">
              <TableHeader>
                <TableRow>
                  <TableHead>Timestamp</TableHead>
                  <TableHead>Type</TableHead>
                  <TableHead>App</TableHead>
                  <TableHead>Message</TableHead>
                  <TableHead>Environment</TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {results.map((r, idx) => (
                  <TableRow
                    key={String(r.timestamp ?? "t") + "-" + idx}
                    className="cursor-pointer"
                    onClick={() => setSelected(r)}
                  >
                    <TableCell className="text-white/60">
                      {r.timestamp ?? "-"}
                    </TableCell>
                    <TableCell
                      className="font-medium"
                      style={{
                        color:
                          levelColor[
                            String(
                              r.type || "",
                            ).toLowerCase() as keyof typeof levelColor
                          ] ?? "inherit",
                      }}
                    >
                      {String(r.type || "").toUpperCase() || "-"}
                    </TableCell>
                    <TableCell className="text-white/80">
                      {r.appName ?? "-"}
                    </TableCell>
                    <TableCell className="text-white/90">
                      {r.message ?? "-"}
                    </TableCell>
                    <TableCell className="text-white/70">
                      {r.environment ?? "-"}
                    </TableCell>
                  </TableRow>
                ))}
              </TableBody>
            </Table>
          )}
        </div>
      </div>

      {selected && (
        <div
          className="fixed inset-0 z-50 bg-black/50"
          onClick={() => setSelected(null)}
        >
          <div
            className="fixed right-0 top-0 h-full w-105 border-l 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"
                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>{selected.timestamp ?? "-"}</span>
                  </div>
                  <div className="flex justify-between">
                    <span className="text-muted-foreground">Type</span>
                    <span
                      className="font-medium"
                      style={{
                        color:
                          levelColor[
                            String(
                              selected.type || "",
                            ).toLowerCase() as keyof typeof levelColor
                          ] ?? "inherit",
                      }}
                    >
                      {String(selected.type || "").toUpperCase() || "-"}
                    </span>
                  </div>
                  <div className="flex justify-between">
                    <span className="text-muted-foreground">App</span>
                    <span>{selected.appName ?? "-"}</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">
                      Environment
                    </div>
                    <div className="rounded-lg border border-white/10 bg-black/20 p-2">
                      {selected.environment ?? "-"}
                    </div>
                  </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">Results</span>
          <span className="font-medium">{stats.total.toLocaleString()}</span>
          <span className="text-muted-foreground">Errors</span>
          <span className="font-medium">{stats.errors.toLocaleString()}</span>
          <span className="text-muted-foreground">Warnings</span>
          <span className="font-medium">{stats.warnings.toLocaleString()}</span>
        </div>
        <div className="text-muted-foreground">
          {isLoading ? "Loading..." : ""}
        </div>
      </div>
    </div>
  );
}

Common mistakes

  • Do not create a second API route. Reuse app/api/oneminutelogs/[type]/route.ts.
  • Initialize the logger before calling getLogs().
  • Use supported filter keys: type, env, appName, search, and limit.
Found something unexpected? Let us know.