├── .nvmrc ├── packages ├── pinorama-client │ ├── README.md │ ├── tests │ │ └── .gitkeep │ ├── src │ │ └── platform │ │ │ ├── node.ts │ │ │ └── browser.ts │ ├── tsconfig.json │ ├── CHANGELOG.md │ ├── package.json │ └── rollup.config.mjs ├── pinorama-server │ ├── README.md │ ├── src │ │ ├── plugins │ │ │ ├── index.mts │ │ │ ├── auth.mts │ │ │ └── graceful-save.mts │ │ ├── routes │ │ │ ├── index.mts │ │ │ ├── introspection.mts │ │ │ ├── persist.mts │ │ │ ├── bulk.mts │ │ │ ├── search.mts │ │ │ └── styles.mts │ │ ├── utils │ │ │ ├── metadata.mts │ │ │ └── styles.mts │ │ └── index.mts │ ├── tsconfig.json │ ├── CHANGELOG.md │ └── package.json ├── pinorama-studio │ ├── src │ │ ├── components │ │ │ ├── kbd │ │ │ │ ├── index.ts │ │ │ │ └── kbd.tsx │ │ │ ├── title-bar │ │ │ │ ├── index.ts │ │ │ │ ├── components │ │ │ │ │ ├── connection-toggle-button.tsx │ │ │ │ │ ├── pinorama-logo.tsx │ │ │ │ │ ├── settings-button.tsx │ │ │ │ │ ├── theme-toggle-button.tsx │ │ │ │ │ ├── hotkeys-button.tsx │ │ │ │ │ └── connection-status-button.tsx │ │ │ │ └── title-bar.tsx │ │ │ ├── empty-state │ │ │ │ ├── index.ts │ │ │ │ └── empty-state.tsx │ │ │ ├── error-state │ │ │ │ ├── index.ts │ │ │ │ └── error-state.tsx │ │ │ ├── icon-button │ │ │ │ ├── index.ts │ │ │ │ └── icon-button.tsx │ │ │ ├── loading-state │ │ │ │ ├── index.ts │ │ │ │ └── loading-state.tsx │ │ │ ├── search-input │ │ │ │ ├── index.ts │ │ │ │ └── search-input.tsx │ │ │ ├── clipboard-button │ │ │ │ ├── index.ts │ │ │ │ └── clipboard-button.tsx │ │ │ └── ui │ │ │ │ ├── skeleton.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── separator.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── tooltip.tsx │ │ │ │ ├── checkbox.tsx │ │ │ │ ├── badge.tsx │ │ │ │ ├── toggle.tsx │ │ │ │ ├── resizable.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── table.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ └── form.tsx │ │ ├── vite-env.d.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── use-pinorama-introspection.ts │ │ │ ├── use-pinorama-styles.ts │ │ │ ├── use-module-hotkeys.ts │ │ │ └── use-pinorama-connection.ts │ │ ├── contexts │ │ │ ├── index.ts │ │ │ ├── pinorama-client-context.tsx │ │ │ ├── i18n-context.tsx │ │ │ ├── app-config-context.tsx │ │ │ └── theme-context.tsx │ │ ├── lib │ │ │ ├── utils.tsx │ │ │ ├── introspection.ts │ │ │ └── modules.tsx │ │ ├── main.tsx │ │ ├── app.tsx │ │ ├── modules │ │ │ ├── log-explorer │ │ │ │ ├── components │ │ │ │ │ ├── log-filters │ │ │ │ │ │ ├── types.ts │ │ │ │ │ │ ├── lib │ │ │ │ │ │ │ ├── utils.tsx │ │ │ │ │ │ │ └── operations.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── hooks │ │ │ │ │ │ │ └── use-facet.ts │ │ │ │ │ │ └── components │ │ │ │ │ │ │ ├── facet-factory-input.tsx │ │ │ │ │ │ │ ├── facet-item.tsx │ │ │ │ │ │ │ ├── facet-header.tsx │ │ │ │ │ │ │ ├── facet-body.tsx │ │ │ │ │ │ │ └── facet.tsx │ │ │ │ │ ├── log-viewer │ │ │ │ │ │ ├── hooks │ │ │ │ │ │ │ ├── use-static-logs.ts │ │ │ │ │ │ │ └── use-live-logs.ts │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ ├── thead.tsx │ │ │ │ │ │ │ ├── tbody.tsx │ │ │ │ │ │ │ └── header │ │ │ │ │ │ │ │ ├── toggle-live-button.tsx │ │ │ │ │ │ │ │ ├── toggle-columns-button.tsx │ │ │ │ │ │ │ │ └── header.tsx │ │ │ │ │ │ └── utils │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── log-details │ │ │ │ │ │ └── styles │ │ │ │ │ │ └── json-viewer.css │ │ │ │ ├── index.ts │ │ │ │ ├── messages │ │ │ │ │ ├── en.json │ │ │ │ │ └── it.json │ │ │ │ └── utils │ │ │ │ │ └── index.ts │ │ │ └── index.tsx │ │ ├── i18n │ │ │ ├── messages │ │ │ │ ├── en.json │ │ │ │ └── it.json │ │ │ └── index.ts │ │ ├── root.tsx │ │ └── styles │ │ │ └── globals.css │ ├── postcss.config.js │ ├── public │ │ └── pinorama-logo.webp │ ├── tsconfig.node.json │ ├── vite.config.ts │ ├── .gitignore │ ├── components.json │ ├── tsconfig.json │ ├── index.html │ ├── CHANGELOG.md │ ├── tailwind.config.cjs │ └── package.json ├── pinorama-docs │ ├── .gitignore │ ├── public │ │ ├── pinorama-logo.webp │ │ └── pinorama-screenshot.webp │ ├── .vitepress │ │ ├── theme │ │ │ └── index.ts │ │ ├── config │ │ │ ├── index.ts │ │ │ └── en.ts │ │ └── style │ │ │ └── vars.css │ ├── CHANGELOG.md │ ├── package.json │ ├── index.md │ └── guide │ │ ├── quick-start.md │ │ └── index.md ├── pinorama-presets │ ├── src │ │ ├── presets │ │ │ ├── index.mts │ │ │ ├── pino.mts │ │ │ └── fastify.mts │ │ └── utils.mts │ ├── CHANGELOG.md │ ├── tsconfig.json │ └── package.json ├── pinorama-transport │ ├── tests │ │ ├── cli.test.mts │ │ ├── unit.test.mts │ │ └── integration.test.mts │ ├── CHANGELOG.md │ ├── tsconfig.json │ ├── package.json │ └── src │ │ ├── lib.mts │ │ └── cli.mts └── pinorama-types │ ├── CHANGELOG.md │ ├── tsconfig.json │ ├── package.json │ └── src │ └── index.ts ├── .github ├── FUNDING.yml └── workflows │ ├── turbo.yml │ ├── docs.yml │ └── release.yml ├── .husky ├── pre-commit └── commit-msg ├── .npmrc ├── pnpm-workspace.yaml ├── commitlint.config.cjs ├── .changeset ├── curvy-coins-kneel.md ├── clean-bobcats-care.md ├── hip-shrimps-rule.md ├── config.json └── README.md ├── .editorconfig ├── turbo.json ├── examples ├── create-server-example │ ├── package.json │ ├── CHANGELOG.md │ └── index.mjs └── fastify-example │ ├── package.json │ ├── CHANGELOG.md │ └── index.mjs ├── .gitignore ├── biome.json ├── .all-contributorsrc ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /packages/pinorama-client/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/pinorama-server/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: cesconix 2 | -------------------------------------------------------------------------------- /packages/pinorama-client/tests/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm biome 2 | pnpm test 3 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | pnpm commitlint --edit "$1" 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts=true 2 | package-manager-strict=false 3 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/kbd/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./kbd" 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "examples/*" 4 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/pinorama-docs/.gitignore: -------------------------------------------------------------------------------- 1 | .vuepress/dist 2 | .vitepress/dist 3 | .vitepress/cache 4 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/title-bar/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./title-bar" 2 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/empty-state/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./empty-state" 2 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/error-state/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./error-state" 2 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/icon-button/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./icon-button" 2 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/loading-state/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./loading-state" 2 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/search-input/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./search-input" 2 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/clipboard-button/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./clipboard-button" 2 | -------------------------------------------------------------------------------- /packages/pinorama-presets/src/presets/index.mts: -------------------------------------------------------------------------------- 1 | export * from "./fastify.mjs" 2 | export * from "./pino.mjs" 3 | -------------------------------------------------------------------------------- /packages/pinorama-server/src/plugins/index.mts: -------------------------------------------------------------------------------- 1 | export * from "./auth.mjs" 2 | export * from "./graceful-save.mjs" 3 | -------------------------------------------------------------------------------- /.changeset/curvy-coins-kneel.md: -------------------------------------------------------------------------------- 1 | --- 2 | "pinorama-studio": patch 3 | --- 4 | 5 | perf(studio): optimize table scrolling 6 | -------------------------------------------------------------------------------- /packages/pinorama-transport/tests/cli.test.mts: -------------------------------------------------------------------------------- 1 | import { describe } from "vitest" 2 | 3 | describe.skip("cli", () => {}) 4 | -------------------------------------------------------------------------------- /packages/pinorama-client/src/platform/node.ts: -------------------------------------------------------------------------------- 1 | import { setTimeout } from "node:timers/promises" 2 | 3 | export { setTimeout } 4 | -------------------------------------------------------------------------------- /.changeset/clean-bobcats-care.md: -------------------------------------------------------------------------------- 1 | --- 2 | "pinorama-server": minor 3 | "pinorama-studio": minor 4 | --- 5 | 6 | update to orama v3 7 | -------------------------------------------------------------------------------- /packages/pinorama-studio/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/pinorama-docs/public/pinorama-logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinoramajs/pinorama/HEAD/packages/pinorama-docs/public/pinorama-logo.webp -------------------------------------------------------------------------------- /packages/pinorama-studio/public/pinorama-logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinoramajs/pinorama/HEAD/packages/pinorama-studio/public/pinorama-logo.webp -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | -------------------------------------------------------------------------------- /packages/pinorama-docs/public/pinorama-screenshot.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinoramajs/pinorama/HEAD/packages/pinorama-docs/public/pinorama-screenshot.webp -------------------------------------------------------------------------------- /packages/pinorama-client/src/platform/browser.ts: -------------------------------------------------------------------------------- 1 | const setTimeout = (ms: number) => 2 | new Promise((resolve) => window.setTimeout(resolve, ms)) 3 | 4 | export { setTimeout } 5 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./use-pinorama-connection" 2 | export * from "./use-pinorama-introspection" 3 | export * from "./use-pinorama-styles" 4 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/contexts/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./app-config-context" 2 | export * from "./i18n-context" 3 | export * from "./pinorama-client-context" 4 | export * from "./theme-context" 5 | -------------------------------------------------------------------------------- /.changeset/hip-shrimps-rule.md: -------------------------------------------------------------------------------- 1 | --- 2 | "pinorama-create-server-example": minor 3 | "pinorama-fastify-example": minor 4 | "pinorama-server": minor 5 | "pinorama-studio": minor 6 | --- 7 | 8 | update to fastify v5 9 | -------------------------------------------------------------------------------- /packages/pinorama-server/src/routes/index.mts: -------------------------------------------------------------------------------- 1 | export * from "./bulk.mjs" 2 | export * from "./introspection.mjs" 3 | export * from "./persist.mjs" 4 | export * from "./search.mjs" 5 | export * from "./styles.mjs" 6 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/lib/utils.tsx: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /packages/pinorama-types/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # pinorama-types 2 | 3 | ## 0.1.0 4 | 5 | ### Minor Changes 6 | 7 | - 4980b4b: add preset feature 8 | 9 | ### Patch Changes 10 | 11 | - a0495f4: add column visibility and sizing 12 | -------------------------------------------------------------------------------- /packages/pinorama-presets/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # pinorama-presets 2 | 3 | ## 0.1.0 4 | 5 | ### Minor Changes 6 | 7 | - 4980b4b: add preset feature 8 | 9 | ### Patch Changes 10 | 11 | - a0495f4: add column visibility and sizing 12 | -------------------------------------------------------------------------------- /packages/pinorama-docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import type { Theme } from "vitepress" 2 | import DefaultTheme from "vitepress/theme" 3 | import "../style/vars.css" 4 | 5 | export default { 6 | extends: DefaultTheme 7 | } satisfies Theme 8 | -------------------------------------------------------------------------------- /packages/pinorama-docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # pinorama-docs 2 | 3 | ## 0.2.0 4 | 5 | ### Minor Changes 6 | 7 | - f28dea5: Home page redesign 8 | 9 | ## 0.1.1 10 | 11 | ### Patch Changes 12 | 13 | - fafcb13: chore(docs): add pinorama github link 14 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client" 2 | import { RootComponent } from "./root" 3 | 4 | const appElement = document.getElementById("app") as HTMLElement 5 | ReactDOM.createRoot(appElement).render() 6 | -------------------------------------------------------------------------------- /packages/pinorama-studio/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/app.tsx: -------------------------------------------------------------------------------- 1 | import { TitleBar } from "@/components/title-bar" 2 | import { Outlet } from "@tanstack/react-router" 3 | 4 | export default function App() { 5 | return ( 6 |
7 | 8 | 9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /packages/pinorama-types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "outDir": "./dist", 8 | "strict": true, 9 | "esModuleInterop": true 10 | }, 11 | "include": ["src"], 12 | "exclude": ["node_modules", "dist"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/components/log-filters/types.ts: -------------------------------------------------------------------------------- 1 | export type FacetValue = { 2 | value: string | number 3 | count: number 4 | } 5 | 6 | export type StringFilter = string[] 7 | export type EnumFilter = { in: (string | number)[] } 8 | export type FacetFilter = StringFilter | EnumFilter 9 | 10 | export type SearchFilters = Record 11 | -------------------------------------------------------------------------------- /packages/pinorama-studio/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path" 2 | import react from "@vitejs/plugin-react" 3 | import { defineConfig } from "vite" 4 | import version from "vite-plugin-package-version" 5 | 6 | export default defineConfig({ 7 | plugins: [react(), version()], 8 | resolve: { 9 | alias: { 10 | "@": path.resolve(__dirname, "./src") 11 | } 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /packages/pinorama-studio/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/pinorama-server/src/routes/introspection.mts: -------------------------------------------------------------------------------- 1 | import type { FastifyInstance } from "fastify" 2 | 3 | export async function introspectionRoute(fastify: FastifyInstance) { 4 | const { introspection } = fastify.pinoramaOpts 5 | 6 | fastify.route({ 7 | url: "/introspection", 8 | method: "get", 9 | handler: async () => { 10 | return introspection 11 | } 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /packages/pinorama-transport/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # pinorama-transport 2 | 3 | ## 0.1.3 4 | 5 | ### Patch Changes 6 | 7 | - 4980b4b: add preset feature 8 | 9 | ## 0.1.2 10 | 11 | ### Patch Changes 12 | 13 | - b31f88c: execute build step on github release workflow 14 | 15 | ## 0.1.1 16 | 17 | ### Patch Changes 18 | 19 | - ed89795: Add repository, bugs, homepage, and author info in `package.json` file 20 | -------------------------------------------------------------------------------- /packages/pinorama-studio/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/app.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/loading-state/loading-state.tsx: -------------------------------------------------------------------------------- 1 | import { LoaderIcon } from "lucide-react" 2 | import { FormattedMessage } from "react-intl" 3 | 4 | export function LoadingState() { 5 | return ( 6 |
7 | 8 | 9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /packages/pinorama-presets/src/utils.mts: -------------------------------------------------------------------------------- 1 | import type { AnySchema } from "@orama/orama" 2 | import type { PinoramaIntrospection } from "pinorama-types" 3 | 4 | type PinoramaPreset = { 5 | schema: T 6 | introspection: PinoramaIntrospection 7 | } 8 | 9 | export function createPreset( 10 | schema: T, 11 | introspection: PinoramaIntrospection 12 | ): PinoramaPreset { 13 | return { schema, introspection } 14 | } 15 | -------------------------------------------------------------------------------- /packages/pinorama-server/src/plugins/auth.mts: -------------------------------------------------------------------------------- 1 | import type { FastifyInstance } from "fastify" 2 | 3 | export async function authHook(fastify: FastifyInstance) { 4 | const { adminSecret } = fastify.pinoramaOpts 5 | 6 | fastify.addHook("preHandler", async (req, res) => { 7 | if (adminSecret && req.headers["x-pinorama-admin-secret"] !== adminSecret) { 8 | res.code(401).send({ error: "unauthorized" }) 9 | throw new Error("unauthorized") 10 | } 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "test": { 5 | "cache": false 6 | }, 7 | "clean": { 8 | "dependsOn": ["^clean"], 9 | "cache": false 10 | }, 11 | "build": { 12 | "dependsOn": ["^build"], 13 | "cache": false 14 | }, 15 | "lint": { 16 | "dependsOn": ["^lint"] 17 | }, 18 | "dev": { 19 | "cache": false, 20 | "persistent": true 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/kbd/kbd.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | export function Kbd({ 4 | children, 5 | className 6 | }: { children: React.ReactNode; className?: string }) { 7 | return ( 8 | 14 | {children} 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/index.tsx: -------------------------------------------------------------------------------- 1 | import type { Module } from "@/lib/modules" 2 | import LogExplorerModule from "./log-explorer" 3 | 4 | const modules: Module[] = [ 5 | LogExplorerModule, 6 | { 7 | id: "feature-fake", 8 | routePath: "/feature2", 9 | component: () => ( 10 |
11 | Welcome to Feature 2! 🎉 12 |
13 | ) 14 | } 15 | ] 16 | 17 | export default modules 18 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /packages/pinorama-server/src/utils/metadata.mts: -------------------------------------------------------------------------------- 1 | import type { AnyOrama } from "@orama/orama" 2 | 3 | export const withPinoramaMetadataSchema = ( 4 | schema: AnyOrama["schema"] 5 | ): AnyOrama["schema"] => { 6 | return { 7 | ...schema, 8 | _pinorama: { 9 | createdAt: "number" 10 | } 11 | } 12 | } 13 | 14 | export const withPinoramaMetadataValue = ( 15 | value: Record 16 | ): Record => { 17 | return { 18 | ...value, 19 | _pinorama: { 20 | createdAt: Date.now() 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/create-server-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pinorama-create-server-example", 3 | "version": "0.1.4", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "start": "node --inspect index.mjs", 8 | "clean": "rimraf node_modules" 9 | }, 10 | "dependencies": { 11 | "@fastify/cors": "^10.0.2", 12 | "@fastify/one-line-logger": "^2.0.2", 13 | "fastify": "^5.2.1", 14 | "pinorama-server": "workspace:*", 15 | "pinorama-transport": "workspace:*" 16 | }, 17 | "devDependencies": { 18 | "rimraf": "^5.0.7" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/fastify-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pinorama-fastify-example", 3 | "version": "0.1.4", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "start": "node --inspect index.mjs", 8 | "clean": "rimraf node_modules" 9 | }, 10 | "dependencies": { 11 | "@fastify/cors": "^10.0.2", 12 | "@fastify/one-line-logger": "^2.0.2", 13 | "fastify": "^5.2.1", 14 | "pinorama-presets": "workspace:*", 15 | "pinorama-server": "workspace:*", 16 | "pinorama-transport": "workspace:*" 17 | }, 18 | "devDependencies": { 19 | "rimraf": "^5.0.7" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/title-bar/components/connection-toggle-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button" 2 | import { usePinoramaConnection } from "@/hooks" 3 | import { FormattedMessage } from "react-intl" 4 | 5 | export function ConnectionToggleButton() { 6 | const { isConnected, toggleConnection } = usePinoramaConnection() 7 | 8 | return ( 9 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/hooks/use-pinorama-introspection.ts: -------------------------------------------------------------------------------- 1 | import { usePinoramaClient } from "@/contexts" 2 | import type { AnySchema } from "@orama/orama" 3 | import { useQuery } from "@tanstack/react-query" 4 | import type { PinoramaIntrospection } from "pinorama-types" 5 | 6 | export const usePinoramaIntrospection = () => { 7 | const client = usePinoramaClient() 8 | 9 | return useQuery({ 10 | queryKey: ["introspection", client], 11 | queryFn: () => 12 | client?.introspection() as Promise>, 13 | staleTime: Number.POSITIVE_INFINITY 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /packages/pinorama-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "skipLibCheck": true, 6 | "target": "ES5", 7 | "allowJs": true, 8 | "resolveJsonModule": true, 9 | "moduleDetection": "force", 10 | "isolatedModules": true, 11 | "strict": true, 12 | "noUncheckedIndexedAccess": true, 13 | "moduleResolution": "Node", 14 | "module": "ESNext", 15 | "sourceMap": true, 16 | "lib": ["ESNext", "DOM"] 17 | }, 18 | "include": ["src"], 19 | "exclude": ["node_modules", "dist", "tests"] 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # Local env files 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Testing 16 | coverage 17 | 18 | # Turbo 19 | .turbo 20 | 21 | # Vercel 22 | .vercel 23 | 24 | # Build Outputs 25 | .next/ 26 | out/ 27 | build 28 | dist 29 | 30 | 31 | # Debug 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | 36 | # Misc 37 | .DS_Store 38 | *.pem 39 | cache 40 | logs 41 | 42 | # Orama 43 | orama_bump_*.json 44 | -------------------------------------------------------------------------------- /packages/pinorama-server/src/plugins/graceful-save.mts: -------------------------------------------------------------------------------- 1 | import { persistToFile } from "@orama/plugin-data-persistence/server" 2 | import type { FastifyInstance } from "fastify" 3 | 4 | export async function gracefulSaveHook(fastify: FastifyInstance) { 5 | fastify.addHook("onClose", async (req) => { 6 | try { 7 | const savedPath = await persistToFile( 8 | fastify.pinoramaDb, 9 | fastify.pinoramaOpts.dbFormat, 10 | fastify.pinoramaOpts.dbPath 11 | ) 12 | req.log.info(`database saved to ${savedPath}`) 13 | } catch (error) { 14 | req.log.error(`failed to save database: ${error}`) 15 | } 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /packages/pinorama-client/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # pinorama-client 2 | 3 | ## 0.2.1 4 | 5 | ### Patch Changes 6 | 7 | - 4980b4b: add preset feature 8 | 9 | ## 0.2.0 10 | 11 | ### Minor Changes 12 | 13 | - 306a952: add live mode feature 14 | 15 | ### Patch Changes 16 | 17 | - 0b8ae5b: chore: optimize code and improve type safety 18 | 19 | ## 0.1.3 20 | 21 | ### Patch Changes 22 | 23 | - b31f88c: execute build step on github release workflow 24 | 25 | ## 0.1.2 26 | 27 | ### Patch Changes 28 | 29 | - ed89795: Add repository, bugs, homepage, and author info in `package.json` file 30 | 31 | ## 0.1.1 32 | 33 | ### Patch Changes 34 | 35 | - 1059f37: replace "docs" with "logs" 36 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/components/log-filters/lib/utils.tsx: -------------------------------------------------------------------------------- 1 | import type { AnySchema } from "@orama/orama" 2 | import type { IntrospectionFacet, PinoramaIntrospection } from "pinorama-types" 3 | 4 | export const getFacetsConfig = ( 5 | introspection: PinoramaIntrospection 6 | ) => { 7 | const definition: { name: string; type: IntrospectionFacet }[] = [] 8 | 9 | const facets = introspection.facets 10 | if (!facets) return { definition } 11 | 12 | for (const [name, config] of Object.entries(facets)) { 13 | definition.push({ 14 | name, 15 | type: config as IntrospectionFacet 16 | }) 17 | } 18 | 19 | return { definition } 20 | } 21 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "linter": { 7 | "enabled": true, 8 | "rules": { 9 | "recommended": true, 10 | "suspicious": { 11 | "noExplicitAny": "off" 12 | } 13 | } 14 | }, 15 | "formatter": { 16 | "enabled": true, 17 | "indentStyle": "space", 18 | "lineWidth": 80 19 | }, 20 | "javascript": { 21 | "formatter": { 22 | "trailingCommas": "none", 23 | "semicolons": "asNeeded" 24 | } 25 | }, 26 | "vcs": { 27 | "enabled": true, 28 | "clientKind": "git", 29 | "useIgnoreFile": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/pinorama-docs/.vitepress/config/index.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitepress" 2 | import { en } from "./en" 3 | 4 | export default defineConfig({ 5 | title: "Pinorama", 6 | 7 | lastUpdated: true, 8 | cleanUrls: true, 9 | metaChunk: true, 10 | 11 | head: [["link", { rel: "icon", href: "/pinorama-logo.webp" }]], 12 | 13 | themeConfig: { 14 | logo: "/pinorama-logo.webp", 15 | 16 | socialLinks: [ 17 | { icon: "github", link: "https://github.com/pinoramajs/pinorama" } 18 | ], 19 | 20 | search: { 21 | provider: "local" 22 | } 23 | }, 24 | 25 | locales: { 26 | root: { label: "English", ...en } 27 | }, 28 | 29 | ignoreDeadLinks: true 30 | }) 31 | -------------------------------------------------------------------------------- /examples/fastify-example/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # pinorama-fastify-example 2 | 3 | ## 0.1.4 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [4980b4b] 8 | - pinorama-transport@0.1.3 9 | - pinorama-server@0.2.1 10 | 11 | ## 0.1.3 12 | 13 | ### Patch Changes 14 | 15 | - Updated dependencies [306a952] 16 | - Updated dependencies [f79e807] 17 | - pinorama-server@0.2.0 18 | - pinorama-transport@0.1.2 19 | 20 | ## 0.1.2 21 | 22 | ### Patch Changes 23 | 24 | - Updated dependencies [b31f88c] 25 | - pinorama-server@0.1.2 26 | - pinorama-transport@0.1.2 27 | 28 | ## 0.1.1 29 | 30 | ### Patch Changes 31 | 32 | - Updated dependencies [ed89795] 33 | - pinorama-transport@0.1.1 34 | - pinorama-server@0.1.1 35 | -------------------------------------------------------------------------------- /packages/pinorama-presets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "skipLibCheck": true, 6 | "target": "es2022", 7 | "allowJs": true, 8 | "resolveJsonModule": true, 9 | "moduleDetection": "force", 10 | "isolatedModules": true, 11 | "strict": true, 12 | "noUncheckedIndexedAccess": true, 13 | "moduleResolution": "NodeNext", 14 | "module": "NodeNext", 15 | "sourceMap": true, 16 | "declaration": true, 17 | "declarationMap": true, 18 | "lib": ["ES2022"], 19 | "outDir": "dist" 20 | }, 21 | "include": ["src"], 22 | "exclude": ["node_modules", "dist", "tests"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/pinorama-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "skipLibCheck": true, 6 | "target": "es2022", 7 | "allowJs": true, 8 | "resolveJsonModule": true, 9 | "moduleDetection": "force", 10 | "isolatedModules": true, 11 | "strict": true, 12 | "noUncheckedIndexedAccess": true, 13 | "moduleResolution": "NodeNext", 14 | "module": "NodeNext", 15 | "sourceMap": true, 16 | "declaration": true, 17 | "declarationMap": true, 18 | "lib": ["ES2022"], 19 | "outDir": "dist" 20 | }, 21 | "include": ["src"], 22 | "exclude": ["node_modules", "dist", "tests"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/title-bar/components/pinorama-logo.tsx: -------------------------------------------------------------------------------- 1 | import { FormattedMessage } from "react-intl" 2 | 3 | export function PinoramaLogo() { 4 | return ( 5 |
6 | Pinorama 7 |
8 | {" "} 9 | 10 | 14 | 15 |
16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /examples/create-server-example/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # pinorama-create-server-example 2 | 3 | ## 0.1.4 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [4980b4b] 8 | - pinorama-transport@0.1.3 9 | - pinorama-server@0.2.1 10 | 11 | ## 0.1.3 12 | 13 | ### Patch Changes 14 | 15 | - Updated dependencies [306a952] 16 | - Updated dependencies [f79e807] 17 | - pinorama-server@0.2.0 18 | - pinorama-transport@0.1.2 19 | 20 | ## 0.1.2 21 | 22 | ### Patch Changes 23 | 24 | - Updated dependencies [b31f88c] 25 | - pinorama-server@0.1.2 26 | - pinorama-transport@0.1.2 27 | 28 | ## 0.1.1 29 | 30 | ### Patch Changes 31 | 32 | - Updated dependencies [ed89795] 33 | - pinorama-transport@0.1.1 34 | - pinorama-server@0.1.1 35 | -------------------------------------------------------------------------------- /packages/pinorama-server/src/routes/persist.mts: -------------------------------------------------------------------------------- 1 | import { persistToFile } from "@orama/plugin-data-persistence/server" 2 | import type { FastifyInstance } from "fastify" 3 | 4 | export async function persistRoute(fastify: FastifyInstance) { 5 | fastify.route({ 6 | url: "/persist", 7 | method: "post", 8 | handler: async (req, res) => { 9 | try { 10 | await persistToFile( 11 | fastify.pinoramaDb, 12 | fastify.pinoramaOpts.dbFormat, 13 | fastify.pinoramaOpts.dbPath 14 | ) 15 | res.code(204).send() 16 | } catch (e) { 17 | req.log.error(e) 18 | res.code(500).send({ error: "failed to persist data" }) 19 | } 20 | } 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /packages/pinorama-server/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # pinorama-server 2 | 3 | ## 0.2.1 4 | 5 | ### Patch Changes 6 | 7 | - 4980b4b: add preset feature 8 | - Updated dependencies [4980b4b] 9 | - Updated dependencies [a0495f4] 10 | - pinorama-presets@0.1.0 11 | - pinorama-types@0.1.0 12 | 13 | ## 0.2.0 14 | 15 | ### Minor Changes 16 | 17 | - 306a952: add live mode feature 18 | 19 | ### Patch Changes 20 | 21 | - f79e807: chore(server): optimize introspection route and improve code structure 22 | 23 | ## 0.1.2 24 | 25 | ### Patch Changes 26 | 27 | - b31f88c: execute build step on github release workflow 28 | 29 | ## 0.1.1 30 | 31 | ### Patch Changes 32 | 33 | - ed89795: Add repository, bugs, homepage, and author info in `package.json` file 34 | -------------------------------------------------------------------------------- /packages/pinorama-server/src/routes/bulk.mts: -------------------------------------------------------------------------------- 1 | import { insertMultiple } from "@orama/orama" 2 | import type { FastifyInstance } from "fastify" 3 | import { withPinoramaMetadataValue } from "../utils/metadata.mjs" 4 | 5 | export async function bulkRoute(fastify: FastifyInstance) { 6 | fastify.route({ 7 | url: "/bulk", 8 | method: "post", 9 | handler: async (req, res) => { 10 | try { 11 | await insertMultiple( 12 | fastify.pinoramaDb, 13 | (req.body as any).map(withPinoramaMetadataValue) 14 | ) 15 | res.code(201).send({ success: true }) 16 | } catch (e) { 17 | req.log.error(e) 18 | res.code(500).send({ error: "failed to insert data" }) 19 | } 20 | } 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /packages/pinorama-transport/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "skipLibCheck": true, 6 | "target": "es2022", 7 | "allowJs": true, 8 | "resolveJsonModule": true, 9 | "moduleDetection": "force", 10 | "isolatedModules": true, 11 | "strict": true, 12 | "noUncheckedIndexedAccess": true, 13 | "moduleResolution": "NodeNext", 14 | "module": "NodeNext", 15 | "sourceMap": true, 16 | "declaration": true, 17 | "declarationMap": true, 18 | "lib": ["ES2022"], 19 | "outDir": "dist", 20 | "types": ["vitest/globals"] 21 | }, 22 | "include": ["src"], 23 | "exclude": ["node_modules", "dist", "tests"] 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/turbo.yml: -------------------------------------------------------------------------------- 1 | name: Turbo CI 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | build_and_test: 11 | name: Build and Test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup PNPM 18 | uses: pnpm/action-setup@v3 19 | 20 | - name: Setup Node.js 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: 20 24 | cache: "pnpm" 25 | 26 | - name: Install 27 | run: pnpm install --no-frozen-lockfile 28 | 29 | - name: Build 30 | run: pnpm build 31 | 32 | - name: Test 33 | run: pnpm test 34 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/error-state/error-state.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | import { FormattedMessage } from "react-intl" 3 | 4 | type ErrorStateProps = { 5 | error: Error 6 | className?: string 7 | } 8 | 9 | export function ErrorState(props: ErrorStateProps) { 10 | return ( 11 |
17 |
18 | 19 | 20 | 21 |
22 |
{props.error.message}
23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /packages/pinorama-server/src/routes/search.mts: -------------------------------------------------------------------------------- 1 | import { type AnyOrama, type Results, search } from "@orama/orama" 2 | import type { FastifyInstance } from "fastify" 3 | import type { PinoramaDocument } from "pinorama-types" 4 | 5 | export async function searchRoute(fastify: FastifyInstance) { 6 | fastify.route({ 7 | url: "/search", 8 | method: "post", 9 | handler: async (req, res) => { 10 | try { 11 | const result: Results> = await search( 12 | fastify.pinoramaDb, 13 | req.body as any 14 | ) 15 | res.code(200).send(result) 16 | } catch (e) { 17 | req.log.error(e) 18 | res.code(500).send({ error: "failed to search data" }) 19 | } 20 | } 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /packages/pinorama-server/src/routes/styles.mts: -------------------------------------------------------------------------------- 1 | import type { FastifyInstance } from "fastify" 2 | import type { IntrospectionStyle } from "pinorama-types" 3 | import { generateCSS } from "../utils/styles.mjs" 4 | 5 | const CSS_MIME_TYPE = "text/css" 6 | 7 | export async function stylesRoute(fastify: FastifyInstance) { 8 | const styles = fastify.pinoramaOpts?.introspection?.styles as 9 | | Record 10 | | undefined 11 | 12 | let css = "" 13 | if (styles) { 14 | css = generateCSS(styles) 15 | } 16 | 17 | fastify.route({ 18 | url: "/styles.css", 19 | method: "get", 20 | handler: (req, res) => { 21 | if (!css) { 22 | req.log.warn("no styles found") 23 | } 24 | res.type(CSS_MIME_TYPE).send(css) 25 | } 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /packages/pinorama-studio/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["./src/*"] 11 | }, 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [ 25 | { 26 | "path": "./tsconfig.node.json" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as LabelPrimitive from "@radix-ui/react-label" 2 | import { type VariantProps, cva } from "class-variance-authority" 3 | import * as React from "react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/title-bar/components/settings-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button" 2 | import { 3 | Tooltip, 4 | TooltipContent, 5 | TooltipPortal, 6 | TooltipTrigger 7 | } from "@/components/ui/tooltip" 8 | import { SettingsIcon } from "lucide-react" 9 | 10 | export function SettingsButton() { 11 | const handleClick = () => {} 12 | 13 | return ( 14 | 15 | 16 | 24 | 25 | 26 | Settings 27 | 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /packages/pinorama-docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pinorama-docs", 3 | "version": "0.2.0", 4 | "private": true, 5 | "description": "Documentation for Pinorama", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vitepress dev", 9 | "build": "vitepress build", 10 | "preview": "vitepress preview", 11 | "clean": "rimraf .vitepress/cache .vitepress/dist node_modules" 12 | }, 13 | "homepage": "https://github.com/pinoramajs/pinorama#readme", 14 | "bugs": { 15 | "url": "https://github.com/pinoramajs/pinorama/issues" 16 | }, 17 | "license": "MIT", 18 | "author": "Francesco Pasqua (https://cesco.me)", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/pinoramajs/pinorama.git", 22 | "directory": "packages/pinorama-docs" 23 | }, 24 | "devDependencies": { 25 | "rimraf": "^5.0.7", 26 | "vitepress": "^1.2.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/index.ts: -------------------------------------------------------------------------------- 1 | import { createModule } from "@/lib/modules" 2 | import { LogExplorer } from "./components/log-explorer" 3 | 4 | export default createModule({ 5 | id: "logExplorer", 6 | component: LogExplorer, 7 | routePath: "/", 8 | messages: { 9 | en: () => import("./messages/en.json"), 10 | it: () => import("./messages/it.json") 11 | }, 12 | hotkeys: { 13 | focusSearch: "/", 14 | showFilters: "f", 15 | showDetails: "o", 16 | maximizeDetails: "m", 17 | liveMode: "l", 18 | refresh: "r", 19 | clearFilters: "x", 20 | selectNextRow: "j, down", 21 | selectPreviousRow: "k, up", 22 | copyToClipboard: "y, c", 23 | incrementFiltersSize: "shift+f", 24 | decrementFiltersSize: "shift+d", 25 | incrementDetailsSize: "shift+j", 26 | decrementDetailsSize: "shift+k", 27 | clearSelection: "esc" 28 | } 29 | } as const) 30 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 2 | import * as React from "react" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const Separator = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >( 10 | ( 11 | { className, orientation = "horizontal", decorative = true, ...props }, 12 | ref 13 | ) => ( 14 | 25 | ) 26 | ) 27 | Separator.displayName = SeparatorPrimitive.Root.displayName 28 | 29 | export { Separator } 30 | -------------------------------------------------------------------------------- /packages/pinorama-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pinorama-types", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "types": "./dist/index.d.ts", 6 | "exports": "./dist/index.js", 7 | "files": ["dist"], 8 | "scripts": { 9 | "clean": "rimraf dist node_modules", 10 | "build": "tsc" 11 | }, 12 | "homepage": "https://github.com/pinoramajs/pinorama#readme", 13 | "bugs": { 14 | "url": "https://github.com/pinoramajs/pinorama/issues" 15 | }, 16 | "license": "MIT", 17 | "author": "Francesco Pasqua (https://cesco.me)", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/pinoramajs/pinorama.git", 21 | "directory": "packages/pinorama-types" 22 | }, 23 | "devDependencies": { 24 | "@orama/orama": "^3.0.4", 25 | "@types/node": "^20.14.2", 26 | "rimraf": "^5.0.7", 27 | "typescript": "^5.4.5" 28 | }, 29 | "publishConfig": { 30 | "access": "public" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "Pinorama", 3 | "projectOwner": "pinoramajs", 4 | "repoType": "github", 5 | "commitConvention": "angular", 6 | "contributors": [ 7 | { 8 | "login": "cesconix", 9 | "name": "Francesco Pasqua", 10 | "avatar_url": "https://avatars.githubusercontent.com/u/244004?v=4", 11 | "profile": "https://cesco.me/", 12 | "contributions": [ 13 | "code", 14 | "design", 15 | "doc", 16 | "ideas", 17 | "maintenance", 18 | "projectManagement", 19 | "review" 20 | ] 21 | }, 22 | { 23 | "login": "ilteoood", 24 | "name": "Matteo Pietro Dazzi", 25 | "avatar_url": "https://avatars.githubusercontent.com/u/6383527?v=4", 26 | "profile": "https://ilteoood.xyz/", 27 | "contributions": ["code", "review", "design"] 28 | } 29 | ], 30 | "files": ["README.md"], 31 | "commitType": "docs", 32 | "contributorsPerLine": 7 33 | } 34 | -------------------------------------------------------------------------------- /packages/pinorama-studio/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Pinorama Studio 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/hooks/use-pinorama-styles.ts: -------------------------------------------------------------------------------- 1 | import { usePinoramaClient } from "@/contexts" 2 | import { useQuery } from "@tanstack/react-query" 3 | 4 | const PINORAMA_STYLES_ID = "pinorama-styles" 5 | 6 | const insertStyle = (data?: string) => { 7 | if (!data) return 8 | 9 | let styleElement = document.getElementById( 10 | PINORAMA_STYLES_ID 11 | ) as HTMLStyleElement 12 | 13 | if (!styleElement) { 14 | styleElement = document.createElement("style") 15 | styleElement.id = PINORAMA_STYLES_ID 16 | document.head.appendChild(styleElement) 17 | } 18 | 19 | styleElement.textContent = data 20 | } 21 | 22 | export const usePinoramaStyles = () => { 23 | const client = usePinoramaClient() 24 | 25 | return useQuery({ 26 | queryKey: ["styles"], 27 | queryFn: async () => { 28 | const response = await client?.styles() 29 | insertStyle(response) 30 | return response 31 | }, 32 | staleTime: Number.POSITIVE_INFINITY 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/i18n/messages/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "app.name": "Pinorama", 3 | "app.version": "v{version}", 4 | "theme.dark": "Dark", 5 | "theme.light": "Light", 6 | "theme.system": "System", 7 | "connection.labels.serverUrl": "Server URL", 8 | "connection.labels.reset": "Reset", 9 | "connection.labels.save": "Save", 10 | "connection.labels.connect": "Connect", 11 | "connection.labels.disconnect": "Disconnect", 12 | "connection.status.disconnected": "Disconnected", 13 | "connection.status.connecting": "Connecting...", 14 | "connection.status.connected": "Connected", 15 | "connection.status.failed": "Connection failed", 16 | "connection.status.unknown": "Unknown", 17 | "labels.inlineError": "Error:", 18 | "labels.loading": "Loading...", 19 | "labels.noResultFound": "No result found", 20 | "labels.copyToClipboard": "Copy to clipboard", 21 | "labels.keyboardShortcuts": "Keyboard Shortcuts", 22 | "labels.allShortcutsSeparatedByModule": "All shortcuts are separated by module" 23 | } 24 | -------------------------------------------------------------------------------- /packages/pinorama-presets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pinorama-presets", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "types": "./dist/presets/index.d.mts", 6 | "exports": "./dist/presets/index.mjs", 7 | "files": ["dist"], 8 | "scripts": { 9 | "clean": "rimraf dist node_modules", 10 | "build": "tsc" 11 | }, 12 | "homepage": "https://github.com/pinoramajs/pinorama#readme", 13 | "bugs": { 14 | "url": "https://github.com/pinoramajs/pinorama/issues" 15 | }, 16 | "license": "MIT", 17 | "author": "Francesco Pasqua (https://cesco.me)", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/pinoramajs/pinorama.git", 21 | "directory": "packages/pinorama-presets" 22 | }, 23 | "devDependencies": { 24 | "@orama/orama": "^3.0.4", 25 | "@types/node": "^20.14.2", 26 | "pinorama-types": "workspace:*", 27 | "rimraf": "^5.0.7", 28 | "typescript": "^5.4.5" 29 | }, 30 | "publishConfig": { 31 | "access": "public" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as PopoverPrimitive from "@radix-ui/react-popover" 2 | import * as React from "react" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const Popover = PopoverPrimitive.Root 7 | 8 | const PopoverTrigger = PopoverPrimitive.Trigger 9 | 10 | const PopoverContent = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 14 | 15 | 25 | 26 | )) 27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 28 | 29 | export { Popover, PopoverTrigger, PopoverContent } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pinorama", 3 | "version": "0.1.0", 4 | "description": "simple, log search for pino", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Francesco Pasqua", 8 | "email": "cesconix@me.com", 9 | "url": "https://github.com/cesconix", 10 | "author": true 11 | }, 12 | "scripts": { 13 | "test": "turbo test", 14 | "clean": "turbo clean && rimraf node_modules .turbo", 15 | "build": "turbo build", 16 | "dev": "turbo dev", 17 | "biome": "biome check --no-errors-on-unmatched --files-ignore-unknown=true", 18 | "changeset": "changeset", 19 | "release": "changeset publish", 20 | "prepare": "husky" 21 | }, 22 | "devDependencies": { 23 | "@biomejs/biome": "^1.9.4", 24 | "@changesets/cli": "^2.27.11", 25 | "@commitlint/cli": "^19.6.1", 26 | "@commitlint/config-conventional": "^19.6.0", 27 | "husky": "^9.1.7", 28 | "rimraf": "^5.0.10", 29 | "turbo": "^2.3.3" 30 | }, 31 | "packageManager": "pnpm@9.2.0", 32 | "engines": { 33 | "node": ">=20" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/components/log-viewer/hooks/use-static-logs.ts: -------------------------------------------------------------------------------- 1 | import { usePinoramaClient } from "@/contexts" 2 | import { buildPayload } from "@/modules/log-explorer/utils" 3 | import { useQuery } from "@tanstack/react-query" 4 | 5 | import type { SearchFilters } from "../../log-filters/types" 6 | 7 | export const useStaticLogs = ( 8 | searchText?: string, 9 | searchFilters?: SearchFilters, 10 | enabled?: boolean 11 | ) => { 12 | const client = usePinoramaClient() 13 | 14 | return useQuery({ 15 | queryKey: ["static-logs", searchText, searchFilters], 16 | queryFn: async ({ signal }) => { 17 | await new Promise((resolve) => setTimeout(resolve, 300)) 18 | if (signal.aborted) return 19 | 20 | const payload = buildPayload(searchText, searchFilters) 21 | 22 | const response = await client?.search(payload) 23 | const newData = response?.hits.map((hit) => hit.document) ?? [] 24 | 25 | return newData 26 | }, 27 | staleTime: Number.POSITIVE_INFINITY, 28 | enabled 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/i18n/messages/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "app.name": "Pinorama", 3 | "app.version": "v{version}", 4 | "theme.dark": "Scuro", 5 | "theme.light": "Chiaro", 6 | "theme.system": "Sistema", 7 | "connection.labels.serverUrl": "URL del server", 8 | "connection.labels.reset": "Reimposta", 9 | "connection.labels.save": "Salva", 10 | "connection.labels.connect": "Connetti", 11 | "connection.labels.disconnect": "Disconnetti", 12 | "connection.status.disconnected": "Disconnesso", 13 | "connection.status.connecting": "Connessione in corso...", 14 | "connection.status.connected": "Connesso", 15 | "connection.status.failed": "Connessione fallita", 16 | "connection.status.unknown": "Sconosciuto", 17 | "labels.inlineError": "Errore:", 18 | "labels.loading": "Caricamento in corso...", 19 | "labels.noResultFound": "Nessun risultato trovato", 20 | "labels.copyToClipboard": "Copia negli appunti", 21 | "labels.keyboardShortcuts": "Scorciatoie da tastiera", 22 | "labels.allShortcutsSeparatedByModule": "Tutte le scorciatoie sono separate per modulo" 23 | } 24 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/components/log-details/styles/json-viewer.css: -------------------------------------------------------------------------------- 1 | .json-view-container { 2 | @apply whitespace-pre-wrap font-mono text-sm pl-1; 3 | } 4 | 5 | .punctuation { 6 | @apply text-muted-foreground; 7 | } 8 | 9 | .expander { 10 | @apply mr-1; 11 | } 12 | 13 | .expand-icon, 14 | .collapse-icon { 15 | @apply inline-flex w-[14px] ml-[-14px] text-lg leading-none text-muted-foreground/40; 16 | } 17 | 18 | .expand-icon::after { 19 | content: "▸"; 20 | } 21 | 22 | .collapse-icon::after { 23 | content: "▾"; 24 | } 25 | 26 | .collapsed-content { 27 | @apply mx-1; 28 | } 29 | 30 | .collapsed-content::after { 31 | content: "..."; 32 | } 33 | 34 | .basic-element-style { 35 | @apply my-1 mx-4 p-0; 36 | } 37 | 38 | .label, 39 | .clickable-label { 40 | @apply mr-2 font-semibold; 41 | } 42 | 43 | .value-string { 44 | @apply text-sky-400; 45 | } 46 | 47 | .value-number, 48 | .value-boolean, 49 | .value-null, 50 | .value-undefined, 51 | .value-other { 52 | @apply text-rose-400; 53 | } 54 | 55 | /* .collapse-icon {} */ 56 | /* .expand-icon {} */ 57 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 2 | import * as React from "react" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const TooltipProvider = TooltipPrimitive.Provider 7 | 8 | const Tooltip = TooltipPrimitive.Root 9 | 10 | const TooltipTrigger = TooltipPrimitive.Trigger 11 | 12 | const TooltipPortal = TooltipPrimitive.Portal 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 27 | )) 28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName 29 | 30 | export { 31 | Tooltip, 32 | TooltipTrigger, 33 | TooltipPortal, 34 | TooltipContent, 35 | TooltipProvider 36 | } 37 | -------------------------------------------------------------------------------- /examples/fastify-example/index.mjs: -------------------------------------------------------------------------------- 1 | import path from "node:path" 2 | import fastifyCors from "@fastify/cors" 3 | import Fastify from "fastify" 4 | import * as pinoramaPresets from "pinorama-presets" 5 | import { fastifyPinoramaServer } from "pinorama-server" 6 | 7 | const fastify = Fastify({ 8 | logger: { 9 | transport: { 10 | targets: [ 11 | { 12 | target: "pinorama-transport", 13 | options: { 14 | url: "http://localhost:6200/my_pinorama_server" 15 | } 16 | }, 17 | { target: "@fastify/one-line-logger" } 18 | ] 19 | } 20 | } 21 | }) 22 | 23 | fastify.register(fastifyCors) 24 | 25 | fastify.register(fastifyPinoramaServer, { 26 | prefix: "/my_pinorama_server", 27 | dbPath: path.resolve("./db.msp"), 28 | dbSchema: pinoramaPresets.fastify.schema, 29 | introspection: pinoramaPresets.fastify.introspection, 30 | logLevel: "silent" // need to avoid loop 31 | }) 32 | 33 | fastify.post("/docs", async function handler(req) { 34 | req.log.info(req.body.message) 35 | return req.body.message 36 | }) 37 | 38 | fastify.listen({ port: 6200 }, (err) => { 39 | if (err) throw err 40 | }) 41 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/title-bar/title-bar.tsx: -------------------------------------------------------------------------------- 1 | import { ConnectionStatusButton } from "./components/connection-status-button" 2 | import { ConnectionToggleButton } from "./components/connection-toggle-button" 3 | import { HotkeysButton } from "./components/hotkeys-button" 4 | import { PinoramaLogo } from "./components/pinorama-logo" 5 | import { ThemeToggleButton } from "./components/theme-toggle-button" 6 | 7 | export function TitleBar() { 8 | return ( 9 |
10 |
11 | {/* Left */} 12 |
13 | 14 |
15 | 16 | {/* Center */} 17 |
18 | 19 |
20 | 21 | {/* Right */} 22 |
23 | 24 | 25 | 26 |
27 |
28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /packages/pinorama-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pinorama-server", 3 | "version": "0.2.1", 4 | "license": "MIT", 5 | "type": "module", 6 | "types": "./dist/index.d.mts", 7 | "exports": "./dist/index.mjs", 8 | "files": ["dist"], 9 | "scripts": { 10 | "clean": "rimraf dist node_modules", 11 | "build": "tsc" 12 | }, 13 | "homepage": "https://github.com/pinoramajs/pinorama#readme", 14 | "bugs": { 15 | "url": "https://github.com/pinoramajs/pinorama/issues" 16 | }, 17 | "author": "Francesco Pasqua (https://cesco.me)", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/pinoramajs/pinorama.git", 21 | "directory": "packages/pinorama-server" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^20.12.7", 25 | "rimraf": "^5.0.7", 26 | "typescript": "^5.4.5" 27 | }, 28 | "dependencies": { 29 | "@orama/orama": "^3.0.4", 30 | "@orama/plugin-data-persistence": "3.0.4", 31 | "change-case": "^5.4.4", 32 | "fastify": "^5.2.1", 33 | "fastify-plugin": "^5.0.1", 34 | "pinorama-presets": "workspace:*", 35 | "pinorama-types": "workspace:*" 36 | }, 37 | "publishConfig": { 38 | "access": "public" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 2 | import { Check } from "lucide-react" 3 | import * as React from "react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Checkbox = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | 22 | 23 | 24 | 25 | )) 26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 27 | 28 | export { Checkbox } 29 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import modules from "@/modules" 2 | 3 | const appMessages: ImportMessages = { 4 | en: () => import("./messages/en.json"), 5 | it: () => import("./messages/it.json") 6 | } 7 | 8 | export type Locale = "en" | "it" 9 | 10 | export type Messages = Record 11 | 12 | export type ImportMessages = { 13 | [key in Locale]: () => Promise<{ 14 | default: Messages 15 | }> 16 | } 17 | 18 | export const getMessages = async (locale: Locale) => { 19 | let messages: Messages = {} 20 | 21 | // App Messages 22 | try { 23 | const module = await appMessages[locale]() 24 | messages = module.default 25 | } catch (error) { 26 | console.warn(`i18n: could not load app messages for "${locale}"`) 27 | } 28 | 29 | // Module Messages 30 | for (const mod of modules) { 31 | const translationImport = mod.messages?.[locale] 32 | if (translationImport) { 33 | try { 34 | const module = await translationImport() 35 | messages = { ...messages, ...module.default } 36 | } catch (error) { 37 | console.warn( 38 | `i18n: could not load "${mod.id}" messages for "${locale}"` 39 | ) 40 | } 41 | } 42 | } 43 | 44 | return messages 45 | } 46 | -------------------------------------------------------------------------------- /packages/pinorama-server/src/utils/styles.mts: -------------------------------------------------------------------------------- 1 | import { kebabCase } from "change-case" 2 | import type { IntrospectionStyle } from "pinorama-types" 3 | 4 | type CSSProperties = Record 5 | 6 | export function generateCSS(styles: Record) { 7 | return Object.entries(styles).map(generateCSSForField).join("") 8 | } 9 | 10 | const createFieldCss = (className: string, style: CSSProperties) => { 11 | return `.${className} { 12 | ${generateCSSProps(style)} 13 | } 14 | ` 15 | } 16 | 17 | const generateCSSForField = ([field, style]: [string, IntrospectionStyle]) => { 18 | const className = `pinorama-${kebabCase(field)}` 19 | 20 | if (!Array.isArray(style)) { 21 | return createFieldCss(className, style) 22 | } 23 | 24 | const [baseStyles, valueStyles] = style 25 | 26 | let fieldCSS = createFieldCss(className, baseStyles) 27 | 28 | for (const [value, valueStyle] of Object.entries(valueStyles)) { 29 | fieldCSS += createFieldCss(`${className}-${kebabCase(value)}`, valueStyle) 30 | } 31 | 32 | return fieldCSS 33 | } 34 | 35 | const generateCSSProps = (style: CSSProperties = {}) => { 36 | return Object.entries(style) 37 | .map(([cssProp, value]) => `${kebabCase(cssProp)}: ${value};`) 38 | .join("") 39 | } 40 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/title-bar/components/theme-toggle-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button" 2 | import { 3 | Tooltip, 4 | TooltipContent, 5 | TooltipPortal, 6 | TooltipTrigger 7 | } from "@/components/ui/tooltip" 8 | import { Theme, useTheme } from "@/contexts" 9 | import { MoonStarIcon, SunIcon } from "lucide-react" 10 | import { FormattedMessage, useIntl } from "react-intl" 11 | 12 | export function ThemeToggleButton() { 13 | const intl = useIntl() 14 | const { theme, setTheme } = useTheme() 15 | 16 | const handleClick = () => { 17 | setTheme(theme === Theme.Light ? Theme.Dark : Theme.Light) 18 | } 19 | 20 | const Icon = theme === Theme.Dark ? MoonStarIcon : SunIcon 21 | 22 | return ( 23 | 24 | 25 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import { type VariantProps, cva } from "class-variance-authority" 2 | import type * as React from "react" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground" 18 | } 19 | }, 20 | defaultVariants: { 21 | variant: "default" 22 | } 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /packages/pinorama-docs/.vitepress/style/vars.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Colors 3 | * -------------------------------------------------------------------------- */ 4 | 5 | :root { 6 | --vp-c-brand-1: #59b9ff; 7 | --vp-c-brand-2: #0a7aff; 8 | --vp-c-brand-3: #0085ff; 9 | } 10 | 11 | .dark { 12 | --vp-c-brand-1: #59b9ff; 13 | --vp-c-brand-2: #0a7aff; 14 | --vp-c-brand-3: #0085ff; 15 | } 16 | 17 | /** 18 | * Component: Button 19 | * -------------------------------------------------------------------------- */ 20 | .dark { 21 | --vp-button-brand-text: #ffffff; 22 | --vp-button-brand-active-text: #ffffff; 23 | --vp-button-brand-hover-text: #ffffff; 24 | } 25 | 26 | /** 27 | * Component: Home 28 | * -------------------------------------------------------------------------- */ 29 | 30 | :root { 31 | --vp-home-hero-name-color: transparent; 32 | --vp-home-hero-name-background: -webkit-linear-gradient( 33 | 120deg, 34 | #0085ff 30%, 35 | #59b9ff 36 | ); 37 | --vp-home-hero-image-background-image: linear-gradient( 38 | -45deg, 39 | #0085ff60 50%, 40 | #59b9ff60 50% 41 | ); 42 | --vp-home-hero-image-filter: blur(30px); 43 | } 44 | 45 | @media (min-width: 640px) { 46 | :root { 47 | --vp-home-hero-image-filter: blur(56px); 48 | } 49 | } 50 | 51 | @media (min-width: 960px) { 52 | :root { 53 | --vp-home-hero-image-filter: blur(72px); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/components/log-filters/index.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react" 2 | 3 | import { Facet } from "./components/facet" 4 | 5 | import type { AnySchema } from "@orama/orama" 6 | import type { PinoramaIntrospection } from "pinorama-types" 7 | import { getFacetsConfig } from "./lib/utils" 8 | import type { SearchFilters } from "./types" 9 | 10 | type PinoramaFacetsProps = { 11 | introspection: PinoramaIntrospection 12 | searchText: string 13 | filters: SearchFilters 14 | liveMode: boolean 15 | onFiltersChange: (filters: SearchFilters) => void 16 | } 17 | 18 | export function LogFilters(props: PinoramaFacetsProps) { 19 | const facetsConfig = useMemo(() => { 20 | return getFacetsConfig(props.introspection) 21 | }, [props.introspection]) 22 | 23 | return ( 24 |
25 | {facetsConfig.definition.map((facet) => { 26 | return ( 27 | 37 | ) 38 | })} 39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docs 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - "packages/pinorama-docs/**" 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | pages: write 13 | id-token: write 14 | 15 | concurrency: 16 | group: pages 17 | cancel-in-progress: false 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | 26 | - name: Setup PNPM 27 | uses: pnpm/action-setup@v3 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: 20 33 | cache: pnpm 34 | 35 | - name: Setup Pages 36 | uses: actions/configure-pages@v4 37 | 38 | - name: Install 39 | run: pnpm install --frozen-lockfile 40 | 41 | - name: Build 42 | run: pnpm --filter pinorama-docs build 43 | 44 | - name: Upload Artifact 45 | uses: actions/upload-pages-artifact@v3 46 | with: 47 | path: packages/pinorama-docs/.vitepress/dist 48 | 49 | deploy: 50 | environment: 51 | name: github-pages 52 | url: ${{ steps.deployment.outputs.page_url }} 53 | needs: build 54 | runs-on: ubuntu-latest 55 | steps: 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v4 59 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Setup PNPM 19 | uses: pnpm/action-setup@v3 20 | 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 20 25 | 26 | - name: Install 27 | run: pnpm install --no-frozen-lockfile 28 | 29 | - name: Build 30 | run: pnpm build 31 | 32 | - name: Creating .npmrc and remove .husky 33 | run: | 34 | cat << EOF > "$HOME/.npmrc" 35 | enable-pre-post-scripts=true 36 | package-manager-strict=false 37 | //registry.npmjs.org/:_authToken=$NPM_TOKEN 38 | EOF 39 | rm -rf .husky 40 | env: 41 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | 43 | - name: Create Release PR or Publish to NPM 44 | id: changesets 45 | uses: changesets/action@v1 46 | with: 47 | publish: pnpm release 48 | commit: "chore: release" 49 | title: "chore: release candidate" 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 53 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/components/log-filters/hooks/use-facet.ts: -------------------------------------------------------------------------------- 1 | import { usePinoramaClient } from "@/contexts" 2 | import { keepPreviousData, useQuery } from "@tanstack/react-query" 3 | 4 | import type { SearchFilters } from "../types" 5 | 6 | type OramaFacetValue = { 7 | count: number 8 | values: Record 9 | } 10 | 11 | const POLL_DELAY = 1500 12 | 13 | export const useFacet = ( 14 | name: string, 15 | searchText: string, 16 | filters: SearchFilters, 17 | liveMode: boolean 18 | ) => { 19 | const client = usePinoramaClient() 20 | 21 | const query = useQuery({ 22 | queryKey: ["facets", name, searchText, filters], 23 | queryFn: async ({ signal }) => { 24 | await new Promise((resolve) => setTimeout(resolve, 500)) 25 | 26 | if (signal.aborted) { 27 | return 28 | } 29 | 30 | const payload: any = { 31 | preflight: true, 32 | facets: { [name]: {} } 33 | } 34 | 35 | if (searchText) { 36 | payload.term = searchText 37 | // payload.properties = [name] 38 | } 39 | 40 | if (filters) { 41 | payload.where = filters 42 | } 43 | 44 | const response: any = await client?.search(payload) 45 | return response.facets[name] 46 | }, 47 | placeholderData: keepPreviousData, 48 | refetchInterval: liveMode ? POLL_DELAY : false 49 | }) 50 | 51 | return query 52 | } 53 | -------------------------------------------------------------------------------- /examples/create-server-example/index.mjs: -------------------------------------------------------------------------------- 1 | import path from "node:path" 2 | import fastifyCors from "@fastify/cors" 3 | import Fastify from "fastify" 4 | import { createServer } from "pinorama-server" 5 | 6 | const pinoramaServer = createServer( 7 | { 8 | // prefix: "/my_pinorama_server", 9 | dbPath: path.resolve("./db.msp") 10 | }, 11 | { 12 | logger: { 13 | transport: { 14 | targets: [ 15 | { target: "@fastify/one-line-logger", options: { colorize: true } } 16 | ] 17 | } 18 | } 19 | } 20 | ) 21 | 22 | pinoramaServer.register(fastifyCors) 23 | 24 | pinoramaServer.listen({ port: 6200 }, (err, address) => { 25 | if (err) throw err 26 | console.log(`Pinorama server listening at ${address}`) 27 | }) 28 | 29 | const genericServer = Fastify({ 30 | logger: { 31 | transport: { 32 | targets: [ 33 | { 34 | target: "pinorama-transport", 35 | options: { 36 | // url: "http://localhost:6200/my_pinorama_server" 37 | url: "http://localhost:6200" 38 | } 39 | }, 40 | { target: "@fastify/one-line-logger" } 41 | ] 42 | } 43 | } 44 | }) 45 | 46 | genericServer.post("/docs", async function handler(req) { 47 | req.log.info(req.body.message) 48 | return req.body.message 49 | }) 50 | 51 | genericServer.listen({ port: 3000 }, (err, address) => { 52 | if (err) throw err 53 | console.log(`Generic server listening at ${address}`) 54 | }) 55 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/components/log-filters/components/facet-factory-input.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox } from "@/components/ui/checkbox" 2 | import { facetFilterOperationsFactory } from "../lib/operations" 3 | 4 | import type { IntrospectionFacet } from "pinorama-types" 5 | import type { SearchFilters } from "../types" 6 | 7 | export function FacetFactoryInput(props: { 8 | id: string 9 | type: IntrospectionFacet 10 | name: string 11 | value: string | number 12 | filters: SearchFilters 13 | onFiltersChange: (filters: SearchFilters) => void 14 | }) { 15 | const operations: any = facetFilterOperationsFactory(props.type) 16 | 17 | const criteria = props.filters[props.name] || operations.create() 18 | const checked = operations.exists(props.value, criteria) 19 | 20 | const handleCheckedChange = (checked: boolean) => { 21 | const newCriteria = checked 22 | ? operations.add(criteria, props.value) 23 | : operations.remove(criteria, props.value) 24 | 25 | const filters = { ...props.filters, [props.name]: newCriteria } 26 | 27 | if (operations.length(newCriteria) === 0) { 28 | delete filters[props.name] 29 | } 30 | 31 | props.onFiltersChange(filters) 32 | } 33 | 34 | return ( 35 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/components/log-filters/lib/operations.ts: -------------------------------------------------------------------------------- 1 | import type { IntrospectionFacet } from "pinorama-types" 2 | import type { EnumFilter, StringFilter } from "../types" 3 | 4 | export const facetFilterOperationsFactory = (type: IntrospectionFacet) => { 5 | switch (type) { 6 | case "enum": 7 | return createEnumOperations() 8 | case "string": 9 | return createStringOperations() 10 | default: 11 | throw new Error(`unsupported type "${type}" for facet operations`) 12 | } 13 | } 14 | 15 | type FacetOperations = { 16 | create: () => C 17 | exists: (value: V, criteria: C) => boolean 18 | length: (criteria: C) => number 19 | values: (criteria?: C) => unknown[] 20 | add: (criteria: C, value: V) => C 21 | remove: (criteria: C, value: V) => C 22 | } 23 | 24 | const createEnumOperations = (): FacetOperations< 25 | EnumFilter, 26 | string | number 27 | > => ({ 28 | create: () => ({ in: [] }), 29 | exists: (v, c) => c.in.includes(v), 30 | length: (c) => c.in.length, 31 | values: (c) => c?.in || [], 32 | add: (c, v) => ({ in: [...c.in, v] }), 33 | remove: (c, v) => ({ in: c.in.filter((_v) => _v !== v) }) 34 | }) 35 | 36 | const createStringOperations = (): FacetOperations => ({ 37 | create: () => [], 38 | exists: (v, c) => c.includes(v), 39 | length: (c) => c.length, 40 | values: (c) => c || [], 41 | add: (c, v) => [...c, v], 42 | remove: (c, v) => c.filter((_v) => _v !== v) 43 | }) 44 | -------------------------------------------------------------------------------- /packages/pinorama-transport/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pinorama-transport", 3 | "version": "0.1.3", 4 | "description": "load pino logs into Pinorama", 5 | "type": "module", 6 | "types": "./dist/lib.d.mts", 7 | "exports": "./dist/lib.mjs", 8 | "bin": { 9 | "pino-pinorama": "./dist/cli.mjs" 10 | }, 11 | "files": ["dist"], 12 | "scripts": { 13 | "test": "vitest run", 14 | "test:watch": "vitest", 15 | "test:debug": "vitest --inspect-brk --no-file-parallelism", 16 | "clean": "rimraf dist node_modules", 17 | "build": "tsc", 18 | "postbuild": "chmod u+x ./dist/cli.mjs" 19 | }, 20 | "homepage": "https://github.com/pinoramajs/pinorama#readme", 21 | "bugs": { 22 | "url": "https://github.com/pinoramajs/pinorama/issues" 23 | }, 24 | "license": "MIT", 25 | "author": "Francesco Pasqua (https://cesco.me)", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/pinoramajs/pinorama.git", 29 | "directory": "packages/pinorama-transport" 30 | }, 31 | "devDependencies": { 32 | "@types/minimist": "^1.2.5", 33 | "@types/node": "^20.14.2", 34 | "@vitest/coverage-v8": "^1.6.0", 35 | "pino": "^9.1.0", 36 | "pinorama-client": "workspace:*", 37 | "pinorama-server": "workspace:*", 38 | "pinorama-types": "workspace:*", 39 | "rimraf": "^5.0.7", 40 | "typescript": "^5.4.5", 41 | "vitest": "^1.6.0" 42 | }, 43 | "publishConfig": { 44 | "access": "public" 45 | }, 46 | "dependencies": { 47 | "minimist": "^1.2.8", 48 | "pino-abstract-transport": "^1.2.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/root.tsx: -------------------------------------------------------------------------------- 1 | import "./styles/globals.css" 2 | 3 | import { 4 | RouterProvider, 5 | createRootRoute, 6 | createRoute, 7 | createRouter, 8 | redirect 9 | } from "@tanstack/react-router" 10 | import { StrictMode } from "react" 11 | 12 | import { TooltipProvider } from "@/components/ui/tooltip" 13 | import { 14 | AppConfigProvider, 15 | I18nProvider, 16 | PinoramaClientProvider, 17 | ThemeProvider 18 | } from "@/contexts" 19 | import App from "./app" 20 | import modules from "./modules" 21 | 22 | const rootRoute = createRootRoute({ 23 | component: App 24 | }) 25 | 26 | const routes = modules.map((mod) => 27 | createRoute({ 28 | getParentRoute: () => rootRoute, 29 | path: mod.routePath, 30 | component: mod.component 31 | }) 32 | ) 33 | 34 | const router = createRouter({ 35 | routeTree: rootRoute.addChildren(routes), 36 | defaultPreload: "intent", 37 | defaultStaleTime: 5000, 38 | notFoundRoute: createRoute({ 39 | id: "not-found", 40 | getParentRoute: () => rootRoute, 41 | beforeLoad: () => redirect({ to: "/" }) 42 | }) 43 | }) 44 | 45 | export function RootComponent() { 46 | return ( 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /packages/pinorama-presets/src/presets/pino.mts: -------------------------------------------------------------------------------- 1 | import { createPreset } from "../utils.mjs" 2 | 3 | export const pino = createPreset( 4 | { 5 | time: "number", 6 | level: "enum", 7 | msg: "string", 8 | pid: "enum", 9 | hostname: "string" 10 | }, 11 | { 12 | facets: { 13 | level: "enum", 14 | msg: "string", 15 | pid: "enum", 16 | hostname: "string" 17 | }, 18 | columns: { 19 | time: { visible: true, size: 150 }, 20 | level: { visible: true, size: 70 }, 21 | msg: { visible: true, size: 400 }, 22 | pid: { visible: false, size: 70 }, 23 | hostname: { visible: false, size: 150 } 24 | }, 25 | labels: { 26 | time: "Time", 27 | level: [ 28 | "Level", 29 | { 30 | 10: "TRACE", 31 | 20: "DEBUG", 32 | 30: "INFO", 33 | 40: "WARN", 34 | 50: "ERROR", 35 | 60: "FATAL" 36 | } 37 | ], 38 | msg: "Message", 39 | pid: "PID", 40 | hostname: "Host" 41 | }, 42 | formatters: { 43 | time: "timestamp" 44 | }, 45 | styles: { 46 | time: { 47 | opacity: "0.5" 48 | }, 49 | level: [ 50 | {}, 51 | { 52 | 10: { color: "var(--color-gray-500)" }, 53 | 20: { color: "var(--color-purple-500)" }, 54 | 30: { color: "var(--color-lime-500)" }, 55 | 40: { color: "var(--color-yellow-500)" }, 56 | 50: { color: "var(--color-red-500)" }, 57 | 60: { color: "var(--color-red-500)" } 58 | } 59 | ] 60 | } 61 | } 62 | ) 63 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/contexts/pinorama-client-context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react" 2 | 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query" 4 | import { PinoramaClient } from "pinorama-client/browser" 5 | import type { BaseOramaPinorama } from "pinorama-types" 6 | import { useAppConfig } from "./app-config-context" 7 | 8 | type PinoramaClientProviderProps = { 9 | children: React.ReactNode 10 | } 11 | 12 | const PinoramaClientContext = 13 | createContext | null>(null) 14 | 15 | export function PinoramaClientProvider({ 16 | children 17 | }: PinoramaClientProviderProps) { 18 | const appConfig = useAppConfig() 19 | 20 | const queryClient = new QueryClient({ 21 | defaultOptions: { 22 | queries: { 23 | enabled: appConfig?.config.connectionIntent 24 | } 25 | } 26 | }) 27 | 28 | const pinoramaClient: PinoramaClient | null = appConfig 29 | ?.config.connectionUrl 30 | ? new PinoramaClient({ url: appConfig.config.connectionUrl }) 31 | : null 32 | 33 | return ( 34 | 35 | 36 | {children} 37 | 38 | 39 | ) 40 | } 41 | 42 | export const usePinoramaClient = () => { 43 | const context = useContext(PinoramaClientContext) 44 | 45 | if (context === undefined) { 46 | throw new Error( 47 | "usePinoramaClient must be used within a PinoramaClientProvider" 48 | ) 49 | } 50 | 51 | return context 52 | } 53 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/ui/toggle.tsx: -------------------------------------------------------------------------------- 1 | import * as TogglePrimitive from "@radix-ui/react-toggle" 2 | import { type VariantProps, cva } from "class-variance-authority" 3 | import * as React from "react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const toggleVariants = cva( 8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-transparent", 13 | outline: 14 | "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground" 15 | }, 16 | size: { 17 | default: "h-10 px-3", 18 | sm: "h-9 px-2.5", 19 | lg: "h-11 px-5" 20 | } 21 | }, 22 | defaultVariants: { 23 | variant: "default", 24 | size: "default" 25 | } 26 | } 27 | ) 28 | 29 | const Toggle = React.forwardRef< 30 | React.ElementRef, 31 | React.ComponentPropsWithoutRef & 32 | VariantProps 33 | >(({ className, variant, size, ...props }, ref) => ( 34 | 39 | )) 40 | 41 | Toggle.displayName = TogglePrimitive.Root.displayName 42 | 43 | export { Toggle, toggleVariants } 44 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/icon-button/icon-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button" 2 | import { 3 | Tooltip, 4 | TooltipContent, 5 | TooltipPortal, 6 | TooltipTrigger 7 | } from "@/components/ui/tooltip" 8 | import { cn } from "@/lib/utils" 9 | import { LoaderIcon, type LucideIcon } from "lucide-react" 10 | import type { ComponentProps } from "react" 11 | import { Kbd } from "../kbd/kbd" 12 | 13 | type IconButtonProps = ComponentProps & { 14 | icon: LucideIcon 15 | tooltip?: string 16 | loading?: boolean 17 | keystroke?: string 18 | } 19 | 20 | export function IconButton({ 21 | variant = "outline2", 22 | icon, 23 | tooltip, 24 | keystroke, 25 | loading, 26 | ...props 27 | }: IconButtonProps) { 28 | const Icon = loading ? LoaderIcon : icon 29 | 30 | const Component = ( 31 | 34 | ) 35 | 36 | return tooltip || keystroke 37 | ? withTooltip(Component, tooltip, keystroke) 38 | : Component 39 | } 40 | 41 | function withTooltip( 42 | WrappedComponent: React.ReactNode, 43 | tooltip?: string, 44 | keystroke?: string 45 | ) { 46 | return ( 47 | 48 | {WrappedComponent} 49 | 50 | 51 |
{tooltip}
52 | {keystroke ? {keystroke} : null} 53 |
54 |
55 |
56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/components/log-viewer/components/thead.tsx: -------------------------------------------------------------------------------- 1 | import { type Table, flexRender } from "@tanstack/react-table" 2 | 3 | export function TableHead({ table }: { table: Table }) { 4 | return ( 5 | 6 | {table.getHeaderGroups().map((headerGroup) => ( 7 | 8 | {headerGroup.headers.map((header) => ( 9 | 15 | {header.isPlaceholder 16 | ? null 17 | : flexRender( 18 | header.column.columnDef.header, 19 | header.getContext() 20 | )} 21 | {header.column.getCanResize() && ( 22 |
31 | )} 32 | 33 | ))} 34 | 35 | ))} 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/search-input/search-input.tsx: -------------------------------------------------------------------------------- 1 | import { SearchIcon, XIcon } from "lucide-react" 2 | import { forwardRef } from "react" 3 | import { Kbd } from "../kbd" 4 | import { Button } from "../ui/button" 5 | import { Input } from "../ui/input" 6 | 7 | type SearchInputProps = { 8 | value: string 9 | onChange: (text: string) => void 10 | placeholder: string 11 | keystroke?: string 12 | } 13 | 14 | export const SearchInput = forwardRef(function SearchInput( 15 | props: SearchInputProps, 16 | ref: React.Ref 17 | ) { 18 | const hasValue = props.value.length > 0 19 | 20 | return ( 21 |
22 | 23 | props.onChange(e.target.value)} 30 | onKeyDown={(e) => { 31 | if (e.key === "Escape" && e.target instanceof HTMLInputElement) { 32 | e.target.blur() 33 | } 34 | }} 35 | /> 36 | {hasValue ? ( 37 | 46 | ) : null} 47 | {props.keystroke ? ( 48 | {props.keystroke} 49 | ) : null} 50 |
51 | ) 52 | }) 53 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/messages/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "logExplorer.title": "Log Explorer", 3 | "logExplorer.searchLogs": "Search logs...", 4 | "logExplorer.noLogsFound": "No logs found. Please check filters.", 5 | "logExplorer.liveMode": "Live Mode", 6 | "logExplorer.columns": "Columns", 7 | "logExplorer.resetColumns": "Reset columns", 8 | "logExplorer.notConnected.title": "Not Connected", 9 | "logExplorer.notConnected.message": "You are currently disconnected. Please connect to view logs.", 10 | "logExplorer.notConnected.action": "Connect", 11 | "logExplorer.noDataSelected.title": "No data selected", 12 | "logExplorer.noDataSelected.message": "Select a row to view details.", 13 | "logExplorer.hotkeys.focusSearch": "Focus search bar", 14 | "logExplorer.hotkeys.showFilters": "Show or hide filters", 15 | "logExplorer.hotkeys.showDetails": "Show or hide details", 16 | "logExplorer.hotkeys.maximizeDetails": "Maximize details", 17 | "logExplorer.hotkeys.liveMode": "Enable or disable Live Mode", 18 | "logExplorer.hotkeys.refresh": "Refresh data", 19 | "logExplorer.hotkeys.clearFilters": "Clear all filters", 20 | "logExplorer.hotkeys.selectNextRow": "Select next row", 21 | "logExplorer.hotkeys.selectPreviousRow": "Select previous row", 22 | "logExplorer.hotkeys.copyToClipboard": "Copy to clipboard", 23 | "logExplorer.hotkeys.incrementFiltersSize": "Increase filters area", 24 | "logExplorer.hotkeys.decrementFiltersSize": "Decrease filters area", 25 | "logExplorer.hotkeys.incrementDetailsSize": "Increase details area", 26 | "logExplorer.hotkeys.decrementDetailsSize": "Decrease details area", 27 | "logExplorer.hotkeys.clearSelection": "Clear selection" 28 | } 29 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/clipboard-button/clipboard-button.tsx: -------------------------------------------------------------------------------- 1 | import { CheckIcon, CopyIcon } from "lucide-react" 2 | import { forwardRef, useCallback, useImperativeHandle, useState } from "react" 3 | import { useIntl } from "react-intl" 4 | import { IconButton } from "../icon-button/icon-button" 5 | 6 | type ClipboardButtonProps = { 7 | textToCopy: string 8 | keystroke?: string 9 | } 10 | 11 | export type ImperativeClipboardButtonHandle = { 12 | copyToClipboard: () => void 13 | } 14 | 15 | export const ClipboardButton = forwardRef< 16 | ImperativeClipboardButtonHandle, 17 | ClipboardButtonProps 18 | >(function ClipboardButton(props, ref) { 19 | const intl = useIntl() 20 | const [isCopied, setIsCopied] = useState(false) 21 | 22 | const handleClick = useCallback(async () => { 23 | if (isCopied || !props.textToCopy) { 24 | return 25 | } 26 | 27 | try { 28 | await navigator.clipboard.writeText(props.textToCopy) 29 | setIsCopied(true) 30 | 31 | setTimeout(() => { 32 | setIsCopied(false) 33 | }, 1500) 34 | } catch (err) { 35 | console.error("Failed to copy to clipboard", err) 36 | } 37 | }, [props.textToCopy, isCopied]) 38 | 39 | useImperativeHandle( 40 | ref, 41 | () => ({ 42 | copyToClipboard: handleClick 43 | }), 44 | [handleClick] 45 | ) 46 | 47 | return ( 48 | 57 | ) 58 | }) 59 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 240 10% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 240 10% 3.9%; 13 | --primary: 240 5.9% 10%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 240 4.8% 95.9%; 16 | --secondary-foreground: 240 5.9% 10%; 17 | --muted: 240 4.8% 95.9%; 18 | --muted-foreground: 240 3.8% 46.1%; 19 | --accent: 240 4.8% 95.9%; 20 | --accent-foreground: 240 5.9% 10%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 240 5.9% 90%; 24 | --input: 240 5.9% 90%; 25 | --ring: 240 5.9% 10%; 26 | --radius: 0.5rem; 27 | } 28 | 29 | .dark { 30 | --background: 240 10% 3.9%; 31 | --foreground: 0 0% 98%; 32 | --card: 240 10% 3.9%; 33 | --card-foreground: 0 0% 98%; 34 | --popover: 240 10% 3.9%; 35 | --popover-foreground: 0 0% 98%; 36 | --primary: 0 0% 98%; 37 | --primary-foreground: 240 5.9% 10%; 38 | --secondary: 240 3.7% 15.9%; 39 | --secondary-foreground: 0 0% 98%; 40 | --muted: 240 3.7% 15.9%; 41 | --muted-foreground: 240 5% 64.9%; 42 | --accent: 240 3.7% 15.9%; 43 | --accent-foreground: 0 0% 98%; 44 | --destructive: 0 62.8% 30.6%; 45 | --destructive-foreground: 0 0% 98%; 46 | --border: 240 3.7% 15.9%; 47 | --input: 240 3.7% 15.9%; 48 | --ring: 240 4.9% 83.9%; 49 | } 50 | } 51 | 52 | @layer base { 53 | * { 54 | @apply border-border; 55 | } 56 | 57 | body { 58 | @apply bg-background text-foreground; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/components/log-filters/components/facet-item.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from "@/components/ui/badge" 2 | import { Label } from "@/components/ui/label" 3 | import { createField } from "@/lib/introspection" 4 | import { FacetFactoryInput } from "./facet-factory-input" 5 | 6 | import type { AnySchema } from "@orama/orama" 7 | import type { IntrospectionFacet, PinoramaIntrospection } from "pinorama-types" 8 | import type { SearchFilters } from "../types" 9 | 10 | type FacetItemProps = { 11 | introspection: PinoramaIntrospection 12 | name: string 13 | type: IntrospectionFacet 14 | value: string | number 15 | count: number 16 | filters: SearchFilters 17 | onFiltersChange: (filters: SearchFilters) => void 18 | } 19 | 20 | export function FacetItem(props: FacetItemProps) { 21 | const field = createField(props.name, props.introspection) 22 | 23 | return ( 24 |
25 | 33 | 39 | 43 | {props.count} 44 | 45 |
46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/components/log-viewer/components/tbody.tsx: -------------------------------------------------------------------------------- 1 | import { type Row, flexRender } from "@tanstack/react-table" 2 | import type { Virtualizer } from "@tanstack/react-virtual" 3 | 4 | type TableBodyProps = { 5 | virtualizer: Virtualizer 6 | rows: Row[] 7 | } 8 | 9 | export function TableBody({ virtualizer, rows }: TableBodyProps) { 10 | return ( 11 | 15 | {virtualizer.getVirtualItems().map((virtualItem) => { 16 | const row = rows[virtualItem.index] as Row 17 | const cells = row.getVisibleCells() 18 | 19 | return ( 20 | {}} 25 | className={`select-none cursor-pointer flex absolute hover:bg-muted/50 odd:bg-muted/20 w-full ${row.getIsSelected() ? "bg-muted/75 hover:bg-muted/75 odd:bg-muted/75" : ""}`} 26 | style={{ transform: `translateY(${virtualItem.start}px)` }} 27 | > 28 | {cells.map((cell) => { 29 | return ( 30 | 37 | {flexRender(cell.column.columnDef.cell, cell.getContext())} 38 | 39 | ) 40 | })} 41 | 42 | ) 43 | })} 44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /packages/pinorama-docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | title: Pinorama 5 | titleTemplate: Explore your logs smarter and faster 6 | 7 | hero: 8 | name: Pinorama 9 | text: Explore your logs smarter and faster 10 | tagline: A beautiful yet powerful Node.js log viewer 11 | actions: 12 | - theme: brand 13 | text: Get Started 14 | link: /guide/ 15 | - theme: alt 16 | text: Pinorama Studio 17 | link: /guide/install/ 18 | - theme: alt 19 | text: View on GitHub 20 | link: https://github.com/pinoramajs/pinorama 21 | image: 22 | src: "/pinorama-logo.webp" 23 | alt: Pinorama 24 | 25 | features: 26 | - title: Orama Powered 27 | icon: 🚀 28 | details: Easily filter logs by facets and perform searches with the Orama super powers. 29 | - title: Fastify Integration 30 | icon: 🔋 31 | details: Connect your Fastify app to a Pinorama and send logs with the transport plugin. 32 | - title: Piped Output 33 | icon: 📡 34 | details: Pipe Node.js logs from any process to Pinorama and view them in real-time. 35 | - title: NDJSON Support 36 | icon: 🍋 37 | details: Configure Pinorama to handle NDJSON files and display them in a structured way. 38 | - title: Custom Styles 39 | icon: 🎨 40 | details: Customize log lines with your own colors and styles to make them stand out. 41 | - title: ESM Support 42 | icon: 📦 43 | details: Built with ESM in mind, ensuring it works with modern JS standards. 44 | --- 45 | 46 | 59 | 60 | Pinorama 61 | 62 | 63 | -------------------------------------------------------------------------------- /packages/pinorama-types/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { AnyOrama, AnySchema, Orama, TypedDocument } from "@orama/orama" 2 | 3 | type SafeFlatten = T extends object 4 | ? { [K in keyof T]: SafeFlatten } 5 | : T 6 | 7 | type FlattenedKeys = T extends object 8 | ? { 9 | [K in keyof T]: 10 | | `${K & string}` 11 | | `${K & string}.${FlattenedKeys & string}` 12 | }[keyof T] 13 | : never 14 | 15 | export type PinoramaIntrospection = { 16 | facets?: Partial<{ 17 | [K in FlattenedKeys>]: IntrospectionFacet 18 | }> 19 | columns: Partial<{ 20 | [K in FlattenedKeys>]: IntrospectionColumn 21 | }> 22 | labels?: Partial<{ 23 | [K in FlattenedKeys>]: IntrospectionLabel 24 | }> 25 | formatters?: Partial<{ 26 | [K in FlattenedKeys>]: IntrospectionFormatter 27 | }> 28 | styles?: Partial<{ 29 | [K in FlattenedKeys>]: IntrospectionStyle 30 | }> 31 | } 32 | 33 | export type IntrospectionFacet = "enum" | "string" 34 | 35 | export type IntrospectionColumn = { 36 | visible: boolean 37 | size?: number 38 | } 39 | 40 | export type IntrospectionLabel = 41 | | string 42 | | [string, Record] 43 | 44 | export type IntrospectionFormatter = string | [string, Record] 45 | 46 | export type IntrospectionStyle = 47 | | Record 48 | | [Record, Record>] 49 | 50 | export type PinoramaDocumentMetadata = { 51 | _pinorama: { 52 | createdAt: string 53 | } 54 | } 55 | 56 | const baseSchema = { 57 | _pinorama: { 58 | createdAt: "number" 59 | } 60 | } as const 61 | 62 | export type BaseOramaPinorama = Orama 63 | export type PinoramaDocument = TypedDocument 64 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/messages/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "logExplorer.title": "Log Explorer", 3 | "logExplorer.searchLogs": "Cerca nei log...", 4 | "logExplorer.noLogsFound": "Nessun log trovato. Si prega di controllare i filtri.", 5 | "logExplorer.liveMode": "Modalità Live", 6 | "logExplorer.columns": "Colonne", 7 | "logExplorer.resetColumns": "Reimposta colonne", 8 | "logExplorer.notConnected.title": "Non Connesso", 9 | "logExplorer.notConnected.message": "Sei attualmente disconnesso. Connettiti per visualizzare i log.", 10 | "logExplorer.notConnected.action": "Connetti", 11 | "logExplorer.noDataSelected.title": "Nessun dato selezionato", 12 | "logExplorer.noDataSelected.message": "Seleziona una riga per visualizzarne i dettagli.", 13 | "logExplorer.hotkeys.focusSearch": "Focus sulla barra di ricerca", 14 | "logExplorer.hotkeys.showFilters": "Mostra o nascondi i filtri", 15 | "logExplorer.hotkeys.showDetails": "Mostra o nascondi i dettagli", 16 | "logExplorer.hotkeys.maximizeDetails": "Massimizza i dettagli", 17 | "logExplorer.hotkeys.liveMode": "Abilita o disabilita la modalità Live", 18 | "logExplorer.hotkeys.refresh": "Aggiorna i dati", 19 | "logExplorer.hotkeys.clearFilters": "Cancella tutti i filtri", 20 | "logExplorer.hotkeys.selectNextRow": "Seleziona la riga successiva", 21 | "logExplorer.hotkeys.selectPreviousRow": "Seleziona la riga precedente", 22 | "logExplorer.hotkeys.copyToClipboard": "Copia negli appunti", 23 | "logExplorer.hotkeys.incrementFiltersSize": "Aumenta l'area dei filtri", 24 | "logExplorer.hotkeys.decrementFiltersSize": "Diminuisci l'area dei filtri", 25 | "logExplorer.hotkeys.incrementDetailsSize": "Aumenta l'area dei dettagli", 26 | "logExplorer.hotkeys.decrementDetailsSize": "Diminuisci l'area dei dettagli", 27 | "logExplorer.hotkeys.clearSelection": "Cancella la selezione" 28 | } 29 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/utils/index.ts: -------------------------------------------------------------------------------- 1 | import type { SearchParams } from "@orama/orama" 2 | import type { BaseOramaPinorama } from "pinorama-types" 3 | 4 | const createBasePayload = (): SearchParams => ({ 5 | limit: 10_000, 6 | sortBy: { property: "_pinorama.createdAt" } 7 | }) 8 | 9 | const withSearchText = 10 | (searchText: string) => 11 | (payload: SearchParams): SearchParams => { 12 | return { ...payload, term: searchText } 13 | } 14 | 15 | const withSearchFilters = 16 | ( 17 | searchFilters: SearchParams["where"] 18 | ) => 19 | (payload: SearchParams): SearchParams => { 20 | const where: SearchParams["where"] = payload.where || {} 21 | return { ...payload, where: { ...where, ...searchFilters } } 22 | } 23 | 24 | const withCursor = 25 | (cursor: number) => 26 | (payload: SearchParams): SearchParams => { 27 | const where: SearchParams["where"] = payload.where || {} 28 | return { 29 | ...payload, 30 | where: { ...where, "_pinorama.createdAt": { gt: cursor || 0 } } 31 | } 32 | } 33 | 34 | export const buildPayload = ( 35 | searchText?: string, 36 | searchFilters?: SearchParams["where"], 37 | cursor?: number 38 | ) => { 39 | let payload = createBasePayload() 40 | 41 | if (searchText) { 42 | const addSearchText = withSearchText(searchText) 43 | payload = addSearchText(payload) 44 | } 45 | 46 | if (searchFilters) { 47 | const addSearchFilters = withSearchFilters(searchFilters) 48 | payload = addSearchFilters(payload) 49 | } 50 | 51 | if (cursor) { 52 | const addCursor = withCursor(cursor) 53 | payload = addCursor(payload) 54 | } 55 | 56 | return payload 57 | } 58 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/lib/introspection.ts: -------------------------------------------------------------------------------- 1 | import type { AnySchema } from "@orama/orama" 2 | import { kebabCase } from "change-case" 3 | import { format } from "date-fns" 4 | import type { PinoramaIntrospection } from "pinorama-types" 5 | 6 | export function createField( 7 | fieldName: string, 8 | introspection: PinoramaIntrospection 9 | ) { 10 | const label = introspection.labels?.[fieldName] 11 | const style = introspection.styles?.[fieldName] 12 | const formatter = introspection.formatters?.[fieldName] 13 | 14 | return { 15 | getDisplayLabel() { 16 | return Array.isArray(label) ? label?.[0] : label ? label : fieldName 17 | }, 18 | getValueLabel(value: string | number) { 19 | if (this.hasValueLabels()) { 20 | return label?.[1][value as number] ?? value 21 | } 22 | return value 23 | }, 24 | hasValueStyle() { 25 | return Array.isArray(style) 26 | }, 27 | hasValueLabels() { 28 | return Array.isArray(label) && label.length === 2 29 | }, 30 | getClassName(value: string | number) { 31 | const css: string[] = [] 32 | 33 | if (!style) return css 34 | 35 | const className = `pinorama-${kebabCase(fieldName)}` 36 | css.push(className) 37 | 38 | if (this.hasValueStyle()) { 39 | const [, valueStyles] = style as any 40 | 41 | const valueStyle = valueStyles[value] 42 | if (valueStyle) { 43 | css.push(`${className}-${value.toString().toLowerCase()}`) 44 | } 45 | } 46 | 47 | return css 48 | }, 49 | format(value: string | number) { 50 | if (formatter === "timestamp") { 51 | return format(new Date(value), "MMM dd HH:mm:ss") 52 | } 53 | 54 | if (this.hasValueLabels()) { 55 | return this.getValueLabel(value) 56 | } 57 | 58 | return value 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/empty-state/empty-state.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | import type { ElementType } from "react" 3 | import { Button } from "../ui/button" 4 | 5 | type EmptyStateProps = { 6 | message: string 7 | className?: string 8 | } 9 | 10 | export function EmptyStateInline(props: EmptyStateProps) { 11 | return ( 12 |
18 | {props.message} 19 |
20 | ) 21 | } 22 | 23 | type ButtonProps = { 24 | text: string 25 | onClick: () => void 26 | } 27 | 28 | type EmptyStateBlockProps = { 29 | icon: ElementType 30 | title: string 31 | message: string 32 | buttons?: ButtonProps[] 33 | } 34 | 35 | export function EmptyStateBlock({ 36 | icon: Icon, 37 | title, 38 | message, 39 | buttons 40 | }: EmptyStateBlockProps) { 41 | return ( 42 |
43 |
44 |
45 | 46 |
47 |
{title}
48 |
{message}
49 |
50 | {buttons?.map((button) => ( 51 | 60 | ))} 61 |
62 |
63 |
64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/components/log-filters/components/facet-header.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button" 2 | import { createField } from "@/lib/introspection" 3 | import type { AnySchema } from "@orama/orama" 4 | import { ChevronDown, ChevronRight, CircleX, LoaderIcon } from "lucide-react" 5 | import type { PinoramaIntrospection } from "pinorama-types" 6 | import type React from "react" 7 | 8 | type FacetHeaderProps = { 9 | introspection: PinoramaIntrospection 10 | name: string 11 | loading: boolean 12 | count: number 13 | open: boolean 14 | onClick: () => void 15 | onCountClick: (e: React.MouseEvent) => void 16 | } 17 | 18 | export function FacetHeader(props: FacetHeaderProps) { 19 | const field = createField(props.name, props.introspection) 20 | const ChevronIcon = props.open ? ChevronDown : ChevronRight 21 | 22 | return ( 23 | 48 | ) : null} 49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/ui/resizable.tsx: -------------------------------------------------------------------------------- 1 | import { GripVertical } from "lucide-react" 2 | import * as ResizablePrimitive from "react-resizable-panels" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const ResizablePanelGroup = ({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) => ( 10 | 17 | ) 18 | 19 | const ResizablePanel = ResizablePrimitive.Panel 20 | 21 | const ResizableHandle = ({ 22 | withHandle, 23 | className, 24 | ...props 25 | }: React.ComponentProps & { 26 | withHandle?: boolean 27 | }) => ( 28 | div]:rotate-90", 31 | className 32 | )} 33 | {...props} 34 | > 35 | {withHandle && ( 36 |
37 | 38 |
39 | )} 40 |
41 | ) 42 | 43 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle } 44 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/contexts/i18n-context.tsx: -------------------------------------------------------------------------------- 1 | import { type Locale, type Messages, getMessages } from "@/i18n" 2 | import { useContext, useEffect, useState } from "react" 3 | import { createContext } from "react" 4 | import type React from "react" 5 | import { IntlProvider } from "react-intl" 6 | 7 | type I18nProviderProps = { 8 | children: React.ReactNode 9 | } 10 | 11 | type LocaleContextProps = { 12 | locale: Locale 13 | setLocale: (locale: Locale) => void 14 | } 15 | 16 | const LocaleContext = createContext(undefined) 17 | 18 | const defaultLocale: Locale = "en" 19 | 20 | export function I18nProvider(props: I18nProviderProps) { 21 | const [locale, setLocale] = useState( 22 | (localStorage.getItem("locale") as Locale) ?? defaultLocale 23 | ) 24 | const [messages, setMessages] = useState(null) 25 | 26 | useEffect(() => { 27 | const loadMessages = async () => { 28 | const messages = await getMessages(locale) 29 | setMessages(messages) 30 | } 31 | 32 | loadMessages() 33 | }, [locale]) 34 | 35 | const handleChangeLocale = async (newLocale: Locale) => { 36 | setMessages(null) 37 | const messages = await getMessages(newLocale) 38 | setLocale(newLocale) 39 | setMessages(messages) 40 | localStorage.setItem("locale", newLocale) 41 | } 42 | 43 | if (messages === null) { 44 | return null 45 | } 46 | 47 | return ( 48 | 49 | 50 | {props.children} 51 | 52 | 53 | ) 54 | } 55 | 56 | export const useLocale = (): LocaleContextProps => { 57 | const context = useContext(LocaleContext) 58 | if (!context) { 59 | throw new Error("useLocale must be used within an I18nProvider") 60 | } 61 | return context 62 | } 63 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/hooks/use-module-hotkeys.ts: -------------------------------------------------------------------------------- 1 | import type { Module } from "@/lib/modules" 2 | import modules from "@/modules" 3 | import { useMemo } from "react" 4 | import type { ComponentType } from "react" 5 | import { useIntl } from "react-intl" 6 | 7 | type ModuleHotkey = { 8 | method: string 9 | keystroke: string 10 | description: string 11 | } 12 | 13 | type ModuleMethod> = keyof NonNullable< 14 | M["hotkeys"] 15 | > 16 | 17 | export function useModuleHotkeys>(module: M) { 18 | const intl = useIntl() 19 | 20 | const hotkeysMap = useMemo(() => { 21 | const hotkeys: Partial, ModuleHotkey>> = {} 22 | 23 | const mod = modules.find((m) => m.id === module.id) 24 | if (!mod || !mod.hotkeys) return hotkeys 25 | 26 | for (const [method, key] of Object.entries(mod.hotkeys)) { 27 | hotkeys[method as ModuleMethod] = { 28 | method, 29 | keystroke: key as string, 30 | description: intl.formatMessage({ id: `${mod.id}.hotkeys.${method}` }) 31 | } 32 | } 33 | 34 | return hotkeys 35 | }, [intl, module.id]) 36 | 37 | const getHotkey = (method: ModuleMethod) => hotkeysMap[method] 38 | 39 | return { hotkeys: hotkeysMap, getHotkey } 40 | } 41 | 42 | export function useAllModuleHotkeys() { 43 | const intl = useIntl() 44 | 45 | return useMemo(() => { 46 | const hotkeys: Record = {} 47 | 48 | for (const mod of modules) { 49 | if (!mod.hotkeys) continue 50 | 51 | const moduleTitle = intl.formatMessage({ id: `${mod.id}.title` }) 52 | 53 | hotkeys[moduleTitle] = Object.entries(mod.hotkeys).map( 54 | ([method, key]) => ({ 55 | method, 56 | keystroke: key as string, 57 | description: intl.formatMessage({ id: `${mod.id}.hotkeys.${method}` }) 58 | }) 59 | ) 60 | } 61 | 62 | return hotkeys 63 | }, [intl]) 64 | } 65 | -------------------------------------------------------------------------------- /packages/pinorama-transport/tests/unit.test.mts: -------------------------------------------------------------------------------- 1 | import { PinoramaClient } from "pinorama-client" 2 | import { afterEach, describe, expect, it, vi } from "vitest" 3 | import pinoramaTransport, { filterOptions } from "../src/lib.mts" 4 | 5 | const exampleOptions = { 6 | url: "http://example.com", 7 | maxRetries: 3, 8 | backoff: 500, 9 | backoffFactor: 2, 10 | backoffMax: 10000, 11 | adminSecret: "secret123", 12 | batchSize: 50, // not a client option 13 | flushInterval: 2000 // not a client option 14 | } 15 | 16 | vi.mock("pinorama-client", async (importOriginal) => { 17 | const mod = await importOriginal() 18 | return { 19 | ...mod, 20 | PinoramaClient: vi.fn() 21 | } 22 | }) 23 | 24 | describe("pinoramaTransport", () => { 25 | const mockedClient = vi.mocked(PinoramaClient) 26 | 27 | afterEach(() => { 28 | vi.restoreAllMocks() 29 | }) 30 | 31 | it("should initialize client with correct options", async () => { 32 | pinoramaTransport(exampleOptions) 33 | 34 | expect(mockedClient).toHaveBeenCalledWith({ 35 | url: "http://example.com", 36 | maxRetries: 3, 37 | backoff: 500, 38 | backoffFactor: 2, 39 | backoffMax: 10000, 40 | adminSecret: "secret123" 41 | }) 42 | }) 43 | }) 44 | 45 | describe("filterOptions", () => { 46 | it("should return an object with only specified keys", async () => { 47 | const filtered = filterOptions(exampleOptions, [ 48 | "url", 49 | "maxRetries", 50 | "backoff" 51 | ]) 52 | 53 | expect(filtered).toEqual({ 54 | url: exampleOptions.url, 55 | maxRetries: exampleOptions.maxRetries, 56 | backoff: exampleOptions.backoff 57 | }) 58 | }) 59 | 60 | it("should not modify the original options object", async () => { 61 | const originalOptions = { ...exampleOptions } 62 | filterOptions(exampleOptions, ["url"]) 63 | 64 | expect(exampleOptions).toEqual(originalOptions) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/contexts/app-config-context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useEffect, useState } from "react" 2 | 3 | export type AppConfig = { 4 | connectionIntent: boolean 5 | connectionUrl?: string | null 6 | liveMode: boolean 7 | } 8 | 9 | type AppConfigContextType = { 10 | config: AppConfig 11 | setConfig: (config: AppConfig) => void 12 | } 13 | 14 | const DEFAULT_CONFIG: AppConfig = { 15 | connectionIntent: false, 16 | connectionUrl: "http://localhost:6200", 17 | liveMode: false 18 | } 19 | 20 | const getAppConfigFromQueryParams = () => { 21 | const appConfig: Partial = {} 22 | const params = new URLSearchParams(window.location.search) 23 | 24 | const connectionUrl = params.get("connectionUrl") 25 | if (connectionUrl) { 26 | appConfig.connectionUrl = connectionUrl 27 | } 28 | 29 | const liveMode = params.get("liveMode") 30 | if (liveMode) { 31 | appConfig.liveMode = liveMode === "true" 32 | } 33 | 34 | return appConfig 35 | } 36 | 37 | const AppConfigContext = createContext(null) 38 | 39 | export function AppConfigProvider(props: { children: React.ReactNode }) { 40 | const [config, setConfig] = useState(DEFAULT_CONFIG) 41 | 42 | useEffect(() => { 43 | const queryConfig = getAppConfigFromQueryParams() 44 | 45 | const autoConnect = !!queryConfig.connectionUrl 46 | if (autoConnect) { 47 | queryConfig.connectionIntent = true 48 | } 49 | 50 | setConfig((prevConfig) => ({ ...prevConfig, ...queryConfig })) 51 | }, []) 52 | 53 | return ( 54 | 55 | {props.children} 56 | 57 | ) 58 | } 59 | 60 | export const useAppConfig = () => { 61 | const context = useContext(AppConfigContext) 62 | 63 | if (context === undefined) { 64 | throw new Error("useAppConfig must be used within a AppConfigProvider") 65 | } 66 | 67 | return context 68 | } 69 | -------------------------------------------------------------------------------- /packages/pinorama-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pinorama-client", 3 | "version": "0.2.1", 4 | "license": "MIT", 5 | "main": "./dist/node/pinorama-client.cjs", 6 | "module": "./dist/node/pinorama-client.mjs", 7 | "exports": { 8 | ".": { 9 | "types": "./dist/types/pinorama-client.d.ts", 10 | "require": "./dist/node/pinorama-client.cjs", 11 | "import": "./dist/node/pinorama-client.mjs" 12 | }, 13 | "./node": { 14 | "types": "./dist/types/pinorama-client.d.ts", 15 | "require": "./dist/node/pinorama-client.cjs", 16 | "import": "./dist/node/pinorama-client.mjs" 17 | }, 18 | "./browser": { 19 | "types": "./dist/types/pinorama-client.d.ts", 20 | "import": "./dist/browser/pinorama-client.esm.js" 21 | } 22 | }, 23 | "types": "./dist/types/pinorama-client.d.ts", 24 | "files": ["dist"], 25 | "scripts": { 26 | "clean": "rimraf dist node_modules", 27 | "build": "rollup --config" 28 | }, 29 | "homepage": "https://github.com/pinoramajs/pinorama#readme", 30 | "bugs": { 31 | "url": "https://github.com/pinoramajs/pinorama/issues" 32 | }, 33 | "author": "Francesco Pasqua (https://cesco.me)", 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/pinoramajs/pinorama.git", 37 | "directory": "packages/pinorama-client" 38 | }, 39 | "devDependencies": { 40 | "@orama/orama": "^3.0.4", 41 | "@rollup/plugin-alias": "^5.1.0", 42 | "@rollup/plugin-commonjs": "^26.0.1", 43 | "@rollup/plugin-node-resolve": "^15.2.3", 44 | "@rollup/plugin-terser": "^0.4.4", 45 | "@rollup/plugin-typescript": "^11.1.6", 46 | "@types/node": "^20.14.2", 47 | "pinorama-types": "workspace:*", 48 | "rimraf": "^5.0.7", 49 | "rollup": "^4.18.0", 50 | "rollup-plugin-dts": "^6.1.1", 51 | "tslib": "^2.6.3", 52 | "typescript": "^5.4.5" 53 | }, 54 | "dependencies": { 55 | "zod": "^3.23.8" 56 | }, 57 | "publishConfig": { 58 | "access": "public" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/pinorama-studio/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # pinorama-studio 2 | 3 | ## 0.4.0 4 | 5 | ### Minor Changes 6 | 7 | - f7dc713: keyboard shortcut system and ui enhancements 8 | 9 | ### Patch Changes 10 | 11 | - 06e7ae4: fix cli help message for batchSize and flushInterval 12 | 13 | ## 0.3.0 14 | 15 | ### Minor Changes 16 | 17 | - 4980b4b: add preset feature 18 | - a0495f4: add column visibility and sizing 19 | 20 | ### Patch Changes 21 | 22 | - 81c2135: set high frequency log flushing defaults 23 | - Updated dependencies [4980b4b] 24 | - Updated dependencies [a0495f4] 25 | - pinorama-presets@0.1.0 26 | - pinorama-transport@0.1.3 27 | - pinorama-client@0.2.1 28 | - pinorama-server@0.2.1 29 | 30 | ## 0.2.2 31 | 32 | ### Patch Changes 33 | 34 | - b33e94d: add refresh data button 35 | 36 | ## 0.2.1 37 | 38 | ### Patch Changes 39 | 40 | - 2ff89b8: wrong query param for connectionUrl 41 | 42 | ## 0.2.0 43 | 44 | ### Minor Changes 45 | 46 | - 306a952: add live mode feature 47 | 48 | ### Patch Changes 49 | 50 | - 54639cb: update favicon to official logo 51 | - 0b8ae5b: chore: optimize code and improve type safety 52 | - Updated dependencies [0b8ae5b] 53 | - Updated dependencies [306a952] 54 | - Updated dependencies [f79e807] 55 | - pinorama-client@0.2.0 56 | - pinorama-server@0.2.0 57 | - pinorama-transport@0.1.2 58 | 59 | ## 0.1.4 60 | 61 | ### Patch Changes 62 | 63 | - Updated dependencies [b31f88c] 64 | - pinorama-client@0.1.3 65 | - pinorama-server@0.1.2 66 | - pinorama-transport@0.1.2 67 | 68 | ## 0.1.3 69 | 70 | ### Patch Changes 71 | 72 | - 5a53ded: execute build step on github release workflow 73 | 74 | ## 0.1.2 75 | 76 | ### Patch Changes 77 | 78 | - ed89795: Add repository, bugs, homepage, and author info in `package.json` file 79 | - Updated dependencies [ed89795] 80 | - pinorama-transport@0.1.1 81 | - pinorama-client@0.1.2 82 | - pinorama-server@0.1.1 83 | 84 | ## 0.1.1 85 | 86 | ### Patch Changes 87 | 88 | - Updated dependencies [1059f37] 89 | - pinorama-client@0.1.1 90 | - pinorama-transport@0.1.0 91 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/components/log-filters/components/facet-body.tsx: -------------------------------------------------------------------------------- 1 | import { FacetItem } from "./facet-item" 2 | 3 | import type { AnySchema } from "@orama/orama" 4 | import { useVirtualizer } from "@tanstack/react-virtual" 5 | import type { IntrospectionFacet, PinoramaIntrospection } from "pinorama-types" 6 | import { useRef } from "react" 7 | import type { FacetValue, SearchFilters } from "../types" 8 | 9 | type FacetBodyProps = { 10 | introspection: PinoramaIntrospection 11 | name: string 12 | type: IntrospectionFacet 13 | values: FacetValue[] 14 | filters: SearchFilters 15 | onFiltersChange: (filters: SearchFilters) => void 16 | } 17 | 18 | export function FacetBody(props: FacetBodyProps) { 19 | const parentRef = useRef(null) 20 | 21 | const rowVirtualizer = useVirtualizer({ 22 | count: props.values.length, 23 | getScrollElement: () => parentRef.current, 24 | estimateSize: () => 38 25 | }) 26 | 27 | return ( 28 |
32 |
36 | {rowVirtualizer.getVirtualItems().map((virtualItem) => ( 37 |
45 | 54 |
55 | ))} 56 |
57 |
58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/contexts/theme-context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useEffect, useState } from "react" 2 | 3 | type Theme = "dark" | "light" | "system" 4 | 5 | type ThemeProviderProps = { 6 | children: React.ReactNode 7 | defaultTheme?: Theme 8 | storageKey?: string 9 | } 10 | 11 | type ThemeProviderState = { 12 | theme: Theme 13 | setTheme: (theme: Theme) => void 14 | } 15 | 16 | export const Theme = Object.freeze({ 17 | Dark: "dark", 18 | Light: "light", 19 | System: "system" 20 | }) satisfies Readonly, Theme>> 21 | 22 | const initialState: ThemeProviderState = { 23 | theme: Theme.System, 24 | setTheme: () => null 25 | } 26 | 27 | const ThemeProviderContext = createContext(initialState) 28 | 29 | export function ThemeProvider({ 30 | children, 31 | defaultTheme = Theme.Dark, 32 | storageKey = "pinorama-theme", 33 | ...props 34 | }: ThemeProviderProps) { 35 | const [theme, setTheme] = useState( 36 | () => (localStorage.getItem(storageKey) as Theme) || defaultTheme 37 | ) 38 | 39 | useEffect(() => { 40 | const root = window.document.documentElement 41 | 42 | root.classList.remove(Theme.Light, Theme.Dark) 43 | 44 | if (theme === Theme.System) { 45 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") 46 | .matches 47 | ? Theme.Dark 48 | : Theme.Light 49 | 50 | root.classList.add(systemTheme) 51 | return 52 | } 53 | 54 | root.classList.add(theme) 55 | }, [theme]) 56 | 57 | const value = { 58 | theme, 59 | setTheme: (theme: Theme) => { 60 | localStorage.setItem(storageKey, theme) 61 | setTheme(theme) 62 | } 63 | } 64 | 65 | return ( 66 | 67 | {children} 68 | 69 | ) 70 | } 71 | 72 | export const useTheme = () => { 73 | const context = useContext(ThemeProviderContext) 74 | 75 | if (context === undefined) 76 | throw new Error("useTheme must be used within a ThemeProvider") 77 | 78 | return context 79 | } 80 | -------------------------------------------------------------------------------- /packages/pinorama-docs/guide/quick-start.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | # Quick Start 6 | 7 | Get up and running with **Pinorama Studio** in minutes! This guide will walk you through the process of installing Pinorama Studio and integrating it with a simple Node.js application. 8 | 9 | ## Prerequisites 10 | 11 | - [Node.js](https://nodejs.org/) version 20 or higher. 12 | - Terminal for starting the Pinorama Studio CLI. 13 | 14 | ## Installation 15 | 16 | Install Pinorama Studio globally: 17 | 18 | ::: code-group 19 | 20 | ```sh [npm] 21 | npm i -g pinorama-studio 22 | ``` 23 | 24 | ```sh [pnpm] 25 | pnpm i -g pinorama-studio 26 | ``` 27 | 28 | ```sh [yarn] 29 | yarn global add pinorama-studio 30 | ``` 31 | 32 | ::: 33 | 34 | ## Quick Setup 35 | 36 | Let's set up a minimal Fastify application with Pinorama Studio for log viewing: 37 | 38 | 1. Create a new directory: 39 | 40 | ```sh 41 | mkdir pinorama-demo && cd pinorama-demo 42 | ``` 43 | 44 | 2. Install Fastify: 45 | 46 | ::: code-group 47 | 48 | ```sh [npm] 49 | npm add fastify 50 | ``` 51 | 52 | ```sh [pnpm] 53 | pnpm add fastify 54 | ``` 55 | 56 | ```sh [yarn] 57 | yarn add fastify 58 | ``` 59 | 60 | ::: 61 | 62 | 3. Create an `index.js` file with the following content: 63 | 64 | ```javascript 65 | const fastify = require("fastify")({ 66 | logger: true, // needed for piping logs to Pinorama! 67 | }); 68 | 69 | fastify.get("/", async (request, reply) => { 70 | request.log.info("Pinorama is awesome! 🚀"); 71 | return { hello: "world" }; 72 | }); 73 | 74 | fastify.listen({ port: 3000 }); 75 | ``` 76 | 77 | 4. Run your application and pipe the output to Pinorama Studio: 78 | 79 | ```sh 80 | node index.js | pinorama --open 81 | ``` 82 | 83 | This command will start your Fastify application, pipe its logs to Pinorama Studio, and open the Pinorama web interface in your default browser. 84 | 85 | ## Next Steps 86 | 87 | Check out the full Pinorama documentation for information on customizing your logging setup and using other Pinorama components. 88 | 89 | Happy logging with Pinorama! 90 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/components/log-viewer/components/header/toggle-live-button.tsx: -------------------------------------------------------------------------------- 1 | import { Kbd } from "@/components/kbd/kbd" 2 | import { Toggle } from "@/components/ui/toggle" 3 | import { TooltipContent } from "@/components/ui/tooltip" 4 | import { useModuleHotkeys } from "@/hooks/use-module-hotkeys" 5 | import { cn } from "@/lib/utils" 6 | import LogExplorerModule from "@/modules/log-explorer" 7 | import { Tooltip, TooltipPortal, TooltipTrigger } from "@radix-ui/react-tooltip" 8 | import { PlayCircleIcon, StopCircleIcon } from "lucide-react" 9 | import { useIntl } from "react-intl" 10 | 11 | type ToggleLiveButtonProps = { 12 | pressed?: boolean 13 | onPressedChange?: (pressed: boolean) => void 14 | } 15 | 16 | export function ToggleLiveButton(props: ToggleLiveButtonProps) { 17 | const intl = useIntl() 18 | const moduleHotkeys = useModuleHotkeys(LogExplorerModule) 19 | 20 | const label = intl.formatMessage({ id: "logExplorer.liveMode" }) 21 | const hotkey = moduleHotkeys.getHotkey("liveMode") 22 | const Icon = props.pressed ? StopCircleIcon : PlayCircleIcon 23 | 24 | return ( 25 | 26 | 27 | 37 |
43 | 44 |
{label}
45 |
46 |
47 |
48 | 49 | 50 |
{hotkey?.description}
51 | {hotkey?.keystroke} 52 |
53 |
54 |
55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot" 2 | import { type VariantProps, cva } from "class-variance-authority" 3 | import * as React from "react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | outline2: 18 | "border border-input bg-muted/20 hover:bg-accent hover:text-accent-foreground", 19 | secondary: 20 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 21 | ghost: "hover:bg-accent hover:text-accent-foreground", 22 | link: "text-primary underline-offset-4 hover:underline" 23 | }, 24 | size: { 25 | default: "h-10 px-4 py-2", 26 | badge: "rounded-full px-1 py-0.5", 27 | sm: "h-8 rounded-md px-3", 28 | lg: "h-11 rounded-md px-8", 29 | icon: "h-10 w-10", 30 | xs: "h-6 px-2" 31 | } 32 | }, 33 | defaultVariants: { 34 | variant: "default", 35 | size: "default" 36 | } 37 | } 38 | ) 39 | 40 | export interface ButtonProps 41 | extends React.ButtonHTMLAttributes, 42 | VariantProps { 43 | asChild?: boolean 44 | } 45 | 46 | const Button = React.forwardRef( 47 | ({ className, variant, size, asChild = false, ...props }, ref) => { 48 | const Comp = asChild ? Slot : "button" 49 | return ( 50 | 55 | ) 56 | } 57 | ) 58 | Button.displayName = "Button" 59 | 60 | export { Button, buttonVariants } 61 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/components/log-viewer/components/header/toggle-columns-button.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton } from "@/components/icon-button/icon-button" 2 | import { 3 | DropdownMenu, 4 | DropdownMenuCheckboxItem, 5 | DropdownMenuContent, 6 | DropdownMenuItem, 7 | DropdownMenuSeparator, 8 | DropdownMenuTrigger 9 | } from "@/components/ui/dropdown-menu" 10 | import { createField } from "@/lib/introspection" 11 | import type { AnySchema } from "@orama/orama" 12 | import type { Table } from "@tanstack/react-table" 13 | import { ListChecksIcon } from "lucide-react" 14 | import type { PinoramaIntrospection } from "pinorama-types" 15 | import { FormattedMessage } from "react-intl" 16 | 17 | type ToggleColumnsButtonProps = { 18 | introspection: PinoramaIntrospection 19 | table: Table 20 | } 21 | 22 | export function ToggleColumnsButton(props: ToggleColumnsButtonProps) { 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | {props.table 30 | .getAllColumns() 31 | .filter( 32 | (column) => 33 | typeof column.accessorFn !== "undefined" && column.getCanHide() 34 | ) 35 | .map((column) => { 36 | const field = createField(column.id, props.introspection) 37 | return ( 38 | column.toggleVisibility(!!value)} 46 | > 47 | {field.getDisplayLabel()} 48 | 49 | ) 50 | })} 51 | 52 | props.table.resetColumnVisibility()}> 53 | 54 | 55 | 56 | 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/components/log-viewer/hooks/use-live-logs.ts: -------------------------------------------------------------------------------- 1 | import { usePinoramaClient } from "@/contexts" 2 | import { buildPayload } from "@/modules/log-explorer/utils" 3 | import { useInfiniteQuery } from "@tanstack/react-query" 4 | import { useCallback, useEffect, useMemo, useRef } from "react" 5 | 6 | import type { AnyOrama, SearchParams } from "@orama/orama" 7 | 8 | const POLL_DELAY = 1500 9 | 10 | export const useLiveLogs = ( 11 | searchText?: string, 12 | searchFilters?: SearchParams["where"], 13 | enabled?: boolean 14 | ) => { 15 | const client = usePinoramaClient() 16 | const timeoutRef = useRef(null) 17 | 18 | const query = useInfiniteQuery({ 19 | queryKey: ["live-logs", searchText, searchFilters], 20 | queryFn: async ({ pageParam }) => { 21 | const payload = buildPayload(searchText, searchFilters, pageParam) 22 | 23 | const response = await client?.search(payload) 24 | const newData = response?.hits.map((hit) => hit.document) ?? [] 25 | 26 | if (newData.length > 0) { 27 | const lastItem = newData[newData.length - 1] 28 | const metadata = lastItem._pinorama 29 | pageParam = metadata.createdAt 30 | } 31 | 32 | return { data: newData, nextCursor: pageParam } 33 | }, 34 | initialPageParam: 0, 35 | getNextPageParam: (lastPage) => lastPage.nextCursor, 36 | staleTime: Number.POSITIVE_INFINITY, 37 | // refetchInterval: POLL_DELAY, // NOTE: This is not working as expected, it doesn't use the nextCursor 38 | enabled: false 39 | }) 40 | 41 | const schedulePoll = useCallback(() => { 42 | if (enabled) { 43 | timeoutRef.current = setTimeout(() => { 44 | query.fetchNextPage().finally(schedulePoll) 45 | }, POLL_DELAY) 46 | } 47 | }, [enabled, query.fetchNextPage]) 48 | 49 | useEffect(() => { 50 | if (enabled) { 51 | query.fetchNextPage().finally(() => { 52 | schedulePoll() 53 | }) 54 | } 55 | return () => { 56 | if (timeoutRef.current) { 57 | clearTimeout(timeoutRef.current) 58 | } 59 | } 60 | }, [enabled, query.fetchNextPage, schedulePoll]) 61 | 62 | const flattenedData = useMemo(() => { 63 | return query.data?.pages.flatMap((page) => page.data) ?? [] 64 | }, [query.data]) 65 | 66 | return { ...query, data: flattenedData } 67 | } 68 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/lib/modules.tsx: -------------------------------------------------------------------------------- 1 | import type { ImportMessages } from "@/i18n" 2 | import { 3 | type ComponentRef, 4 | type ComponentType, 5 | useEffect, 6 | useRef, 7 | useState 8 | } from "react" 9 | import { useHotkeys } from "react-hotkeys-hook" 10 | 11 | export type MethodKeys = { 12 | [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never 13 | }[keyof T] 14 | 15 | export type Hotkeys = Record, string> 16 | 17 | export type Module = { 18 | id: string 19 | routePath: string 20 | component: T 21 | messages?: ImportMessages 22 | hotkeys?: Hotkeys> 23 | } 24 | 25 | export function createModule( 26 | mod: Module 27 | ): Module { 28 | return mod.hotkeys 29 | ? { ...mod, component: withHotkeys(mod.component, mod.hotkeys) } 30 | : mod 31 | } 32 | 33 | function withHotkeys( 34 | WrappedComponent: T, 35 | hotkeys: Hotkeys> 36 | ): T { 37 | function ComponentWithHotkeys(props: any) { 38 | const ref = useRef>(null) 39 | const [hotkeyEnabled, setHotkeyEnabled] = useState(true) 40 | 41 | useEffect(() => { 42 | const attrName = "data-scroll-locked" 43 | 44 | const checkDataAttribute = () => { 45 | const bodyDataAttribute = document.body.getAttribute(attrName) 46 | setHotkeyEnabled(bodyDataAttribute !== "1") 47 | } 48 | 49 | checkDataAttribute() 50 | 51 | const observer = new MutationObserver(checkDataAttribute) 52 | observer.observe(document.body, { 53 | attributes: true, 54 | attributeFilter: [attrName] 55 | }) 56 | 57 | return () => observer.disconnect() 58 | }, []) 59 | 60 | for (const [method, key] of Object.entries(hotkeys)) { 61 | useHotkeys( 62 | key as string, 63 | (event) => { 64 | event.preventDefault() 65 | if (ref.current) { 66 | const func = ref.current[method as keyof ComponentRef] 67 | if (typeof func === "function") { 68 | func.call(ref.current) 69 | } else { 70 | console.warn(`Method '${method}' not found or not a function`) 71 | } 72 | } 73 | }, 74 | { 75 | enabled: hotkeyEnabled 76 | } 77 | ) 78 | } 79 | 80 | return 81 | } 82 | 83 | return ComponentWithHotkeys as T 84 | } 85 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/hooks/use-pinorama-connection.ts: -------------------------------------------------------------------------------- 1 | import { useAppConfig } from "@/contexts" 2 | import { useEffect, useState } from "react" 3 | import { usePinoramaIntrospection } from "./use-pinorama-introspection" 4 | import { usePinoramaStyles } from "./use-pinorama-styles" 5 | 6 | export type ConnectionStatus = 7 | | "disconnected" 8 | | "connecting" 9 | | "connected" 10 | | "failed" 11 | | "unknown" 12 | 13 | export const ConnectionStatus = Object.freeze({ 14 | Disconnected: "disconnected", 15 | Connecting: "connecting", 16 | Connected: "connected", 17 | Failed: "failed", 18 | Unknown: "unknown" 19 | }) satisfies Readonly, ConnectionStatus>> 20 | 21 | type ConnectionStatusDetail = { 22 | connectionStatus: ConnectionStatus 23 | isConnected?: boolean 24 | } 25 | 26 | export function usePinoramaConnection() { 27 | const appConfig = useAppConfig() 28 | 29 | const introspection = usePinoramaIntrospection() 30 | usePinoramaStyles() 31 | 32 | const [{ connectionStatus, isConnected = false }, setConnectionStatus] = 33 | useState({ 34 | connectionStatus: ConnectionStatus.Unknown 35 | }) 36 | 37 | useEffect(() => { 38 | switch (true) { 39 | case appConfig?.config.connectionIntent === false: 40 | setConnectionStatus({ connectionStatus: ConnectionStatus.Disconnected }) 41 | break 42 | case introspection.status === "pending" && 43 | introspection.fetchStatus === "fetching": 44 | setConnectionStatus({ connectionStatus: ConnectionStatus.Connecting }) 45 | break 46 | case introspection.status === "success": 47 | setConnectionStatus({ 48 | connectionStatus: ConnectionStatus.Connected, 49 | isConnected: true 50 | }) 51 | break 52 | case introspection.status === "error": 53 | setConnectionStatus({ connectionStatus: ConnectionStatus.Failed }) 54 | break 55 | default: 56 | setConnectionStatus({ connectionStatus: ConnectionStatus.Unknown }) 57 | break 58 | } 59 | }, [ 60 | appConfig?.config.connectionIntent, 61 | introspection.status, 62 | introspection.fetchStatus 63 | ]) 64 | 65 | const toggleConnection = () => { 66 | appConfig?.setConfig({ 67 | ...appConfig.config, 68 | connectionIntent: !appConfig.config.connectionIntent 69 | }) 70 | } 71 | 72 | return { 73 | connectionStatus, 74 | toggleConnection, 75 | isConnected, 76 | introspection: introspection.data 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/title-bar/components/hotkeys-button.tsx: -------------------------------------------------------------------------------- 1 | import { Kbd } from "@/components/kbd/kbd" 2 | import { Button } from "@/components/ui/button" 3 | import { 4 | Dialog, 5 | DialogContent, 6 | DialogDescription, 7 | DialogHeader, 8 | DialogTitle, 9 | DialogTrigger 10 | } from "@/components/ui/dialog" 11 | import { 12 | Tooltip, 13 | TooltipContent, 14 | TooltipPortal, 15 | TooltipTrigger 16 | } from "@/components/ui/tooltip" 17 | import { useAllModuleHotkeys } from "@/hooks/use-module-hotkeys" 18 | import { KeyboardIcon } from "lucide-react" 19 | import { FormattedMessage } from "react-intl" 20 | 21 | export function HotkeysButton() { 22 | const hotkeys = useAllModuleHotkeys() 23 | 24 | const handleClick = () => {} 25 | 26 | return ( 27 | 28 | 29 | 30 | 31 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {Object.entries(hotkeys).map(([module, hotkeys]) => ( 57 |
58 |
{module}
59 |
    60 | {hotkeys.map((hotkey) => ( 61 |
  • 65 | 66 | {hotkey.description} 67 | 68 | {hotkey.keystroke} 69 |
  • 70 | ))} 71 |
72 |
73 | ))} 74 |
75 |
76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /packages/pinorama-studio/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | "./pages/**/*.{ts,tsx}", 6 | "./components/**/*.{ts,tsx}", 7 | "./app/**/*.{ts,tsx}", 8 | "./src/**/*.{ts,tsx}" 9 | ], 10 | prefix: "", 11 | theme: { 12 | container: { 13 | center: true, 14 | padding: "2rem", 15 | screens: { 16 | "2xl": "1400px" 17 | } 18 | }, 19 | extend: { 20 | fontFamily: { 21 | mono: ["IBM Plex Mono"], 22 | sans: ["Roboto"] 23 | }, 24 | colors: { 25 | border: "hsl(var(--border))", 26 | input: "hsl(var(--input))", 27 | ring: "hsl(var(--ring))", 28 | background: "hsl(var(--background))", 29 | foreground: "hsl(var(--foreground))", 30 | primary: { 31 | DEFAULT: "hsl(var(--primary))", 32 | foreground: "hsl(var(--primary-foreground))" 33 | }, 34 | secondary: { 35 | DEFAULT: "hsl(var(--secondary))", 36 | foreground: "hsl(var(--secondary-foreground))" 37 | }, 38 | destructive: { 39 | DEFAULT: "hsl(var(--destructive))", 40 | foreground: "hsl(var(--destructive-foreground))" 41 | }, 42 | muted: { 43 | DEFAULT: "hsl(var(--muted))", 44 | foreground: "hsl(var(--muted-foreground))" 45 | }, 46 | accent: { 47 | DEFAULT: "hsl(var(--accent))", 48 | foreground: "hsl(var(--accent-foreground))" 49 | }, 50 | popover: { 51 | DEFAULT: "hsl(var(--popover))", 52 | foreground: "hsl(var(--popover-foreground))" 53 | }, 54 | card: { 55 | DEFAULT: "hsl(var(--card))", 56 | foreground: "hsl(var(--card-foreground))" 57 | } 58 | }, 59 | borderRadius: { 60 | lg: "var(--radius)", 61 | md: "calc(var(--radius) - 2px)", 62 | sm: "calc(var(--radius) - 4px)" 63 | }, 64 | keyframes: { 65 | "accordion-down": { 66 | from: { height: "0" }, 67 | to: { height: "var(--radix-accordion-content-height)" } 68 | }, 69 | "accordion-up": { 70 | from: { height: "var(--radix-accordion-content-height)" }, 71 | to: { height: "0" } 72 | } 73 | }, 74 | animation: { 75 | "accordion-down": "accordion-down 0.2s ease-out", 76 | "accordion-up": "accordion-up 0.2s ease-out" 77 | } 78 | } 79 | }, 80 | plugins: [ 81 | require("tailwindcss-animate"), 82 | require("@tailwind-plugin/expose-colors")({ prefix: "--color" }) 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/components/log-viewer/utils/index.tsx: -------------------------------------------------------------------------------- 1 | import { createField } from "@/lib/introspection" 2 | import { cn } from "@/lib/utils" 3 | import type { AnySchema } from "@orama/orama" 4 | import type { ColumnDef, Table } from "@tanstack/react-table" 5 | import debounce from "debounce" 6 | import type { PinoramaIntrospection } from "pinorama-types" 7 | 8 | const DEFAULT_COLUMN_SIZE = 150 9 | 10 | export const getColumnsConfig = ( 11 | introspection: PinoramaIntrospection 12 | ) => { 13 | const visibility: Record = {} 14 | const sizing: Record = {} 15 | const definition: ColumnDef[] = [] 16 | 17 | const columns = introspection?.columns 18 | if (!columns) 19 | return { 20 | visibility, 21 | sizing, 22 | definition 23 | } 24 | 25 | for (const [name, config] of Object.entries(columns)) { 26 | // Visibility 27 | visibility[name] = config?.visible ?? false 28 | 29 | // Sizing 30 | sizing[name] = config?.size ?? DEFAULT_COLUMN_SIZE 31 | 32 | // Definition 33 | const field = createField(name, introspection) 34 | definition.push({ 35 | id: name, 36 | // accessorKey: columnName.split(".")[0] || columnName, 37 | accessorKey: name, 38 | header: () => field.getDisplayLabel(), 39 | cell: (info) => { 40 | const value = info.getValue() as string | number 41 | if (!value) { 42 | return
43 | } 44 | 45 | const formattedValue = field.format(value) 46 | const className = field.getClassName(value) 47 | return ( 48 |
49 | {formattedValue} 50 |
51 | ) 52 | } 53 | }) 54 | } 55 | 56 | return { 57 | visibility, 58 | sizing, 59 | definition 60 | } 61 | } 62 | 63 | const debouncedScrollIntoView = debounce((element: Element) => { 64 | element.scrollIntoView({ 65 | block: "center", 66 | behavior: "smooth" 67 | }) 68 | }, 50) 69 | 70 | export const selectRowByIndex = (index: number, table: Table) => { 71 | const totalRows = table.getRowModel().rows.length 72 | const validIndex = Math.max(0, Math.min(index, totalRows - 1)) 73 | 74 | table.setRowSelection({ [index]: true }) 75 | 76 | const row = document.querySelector(`[data-index="${validIndex}"]`) 77 | if (row) { 78 | debouncedScrollIntoView(row) 79 | } 80 | } 81 | 82 | export const getCurrentRowIndex = (table: Table) => { 83 | const selectedKeys = table.getSelectedRowModel() 84 | return selectedKeys.rows[0]?.index ?? -1 85 | } 86 | 87 | export const canSelectNextRow = (table: Table) => { 88 | const currentRowIndex = getCurrentRowIndex(table) 89 | return currentRowIndex < table.getRowModel().rows.length - 1 90 | } 91 | 92 | export const canSelectPreviousRow = (table: Table) => { 93 | const currentRowIndex = getCurrentRowIndex(table) 94 | return currentRowIndex > 0 95 | } 96 | -------------------------------------------------------------------------------- /packages/pinorama-studio/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pinorama-studio", 3 | "version": "0.4.0", 4 | "license": "MIT", 5 | "type": "module", 6 | "bin": { 7 | "pinorama": "./cli.mjs" 8 | }, 9 | "files": ["dist", "cli.mjs"], 10 | "scripts": { 11 | "dev": "vite", 12 | "build": "tsc && vite build", 13 | "preview": "vite preview", 14 | "clean": "rimraf dist node_modules" 15 | }, 16 | "homepage": "https://github.com/pinoramajs/pinorama#readme", 17 | "bugs": { 18 | "url": "https://github.com/pinoramajs/pinorama/issues" 19 | }, 20 | "author": "Francesco Pasqua (https://cesco.me)", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/pinoramajs/pinorama.git", 24 | "directory": "packages/pinorama-studio" 25 | }, 26 | "dependencies": { 27 | "@fastify/cors": "^10.0.2", 28 | "@fastify/one-line-logger": "^2.0.2", 29 | "@fastify/static": "^8.0.4", 30 | "@hookform/resolvers": "^3.9.0", 31 | "@radix-ui/react-checkbox": "^1.1.1", 32 | "@radix-ui/react-dialog": "^1.1.1", 33 | "@radix-ui/react-dropdown-menu": "^2.1.1", 34 | "@radix-ui/react-label": "^2.1.0", 35 | "@radix-ui/react-popover": "^1.1.1", 36 | "@radix-ui/react-select": "^2.1.1", 37 | "@radix-ui/react-separator": "^1.1.0", 38 | "@radix-ui/react-slot": "^1.1.0", 39 | "@radix-ui/react-toast": "^1.2.1", 40 | "@radix-ui/react-toggle": "^1.1.0", 41 | "@radix-ui/react-tooltip": "^1.1.2", 42 | "@tanstack/react-query": "^5.50.1", 43 | "@tanstack/react-router": "^1.45.4", 44 | "@tanstack/react-table": "^8.19.2", 45 | "@tanstack/react-virtual": "^3.8.2", 46 | "chalk": "^5.3.0", 47 | "change-case": "^5.4.4", 48 | "class-variance-authority": "^0.7.0", 49 | "clsx": "^2.1.1", 50 | "date-fns": "^3.6.0", 51 | "debounce": "^2.1.0", 52 | "fastify": "^5.2.1", 53 | "lucide-react": "^0.390.0", 54 | "minimist": "^1.2.8", 55 | "open": "^10.1.0", 56 | "pinorama-client": "workspace:*", 57 | "pinorama-presets": "workspace:*", 58 | "pinorama-server": "workspace:*", 59 | "pinorama-transport": "workspace:*", 60 | "react": "^18.3.1", 61 | "react-dom": "^18.3.1", 62 | "react-hook-form": "^7.52.1", 63 | "react-hotkeys-hook": "^4.5.0", 64 | "react-intl": "^6.6.8", 65 | "react-json-view-lite": "^1.4.0", 66 | "react-resizable-panels": "^2.0.20", 67 | "tailwind-merge": "^2.4.0", 68 | "tailwindcss-animate": "^1.0.7", 69 | "zod": "^3.23.8" 70 | }, 71 | "devDependencies": { 72 | "@orama/orama": "^3.0.4", 73 | "@tailwind-plugin/expose-colors": "^1.1.8", 74 | "@types/node": "^20.14.10", 75 | "@types/react": "^18.3.3", 76 | "@types/react-dom": "^18.3.0", 77 | "@vitejs/plugin-react": "^4.3.1", 78 | "autoprefixer": "^10.4.19", 79 | "pinorama-types": "workspace:*", 80 | "postcss": "^8.4.39", 81 | "rimraf": "^5.0.9", 82 | "tailwindcss": "^3.4.4", 83 | "typescript": "^5.5.3", 84 | "vite": "^5.3.3", 85 | "vite-plugin-package-version": "^1.1.0" 86 | }, 87 | "publishConfig": { 88 | "access": "public" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/pinorama-client/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import alias from "@rollup/plugin-alias" 2 | import commonjs from "@rollup/plugin-commonjs" 3 | import resolve from "@rollup/plugin-node-resolve" 4 | import terser from "@rollup/plugin-terser" 5 | import typescript from "@rollup/plugin-typescript" 6 | import { dts } from "rollup-plugin-dts" 7 | 8 | const inputFile = "src/index.ts" 9 | const outputFileName = "pinorama-client" 10 | 11 | export default [ 12 | // Declaration file 13 | { 14 | input: inputFile, 15 | output: [{ file: "dist/types/pinorama-client.d.ts", format: "es" }], 16 | plugins: [dts()] 17 | }, 18 | 19 | // Browser ESM build 20 | { 21 | input: inputFile, 22 | output: [ 23 | { 24 | file: `dist/browser/${outputFileName}.esm.js`, 25 | format: "es", 26 | sourcemap: true 27 | } 28 | ], 29 | plugins: [ 30 | alias({ 31 | entries: [ 32 | { find: "./platform/node.js", replacement: "./platform/browser.js" } 33 | ] 34 | }), 35 | resolve({ browser: true }), 36 | commonjs(), 37 | typescript(), 38 | terser() 39 | ], 40 | external: ["zod"] 41 | }, 42 | 43 | // // Browser UMD build 44 | // { 45 | // input: inputFile, 46 | // output: [ 47 | // { 48 | // file: `dist/browser/${outputFileName}.umd.js`, 49 | // format: "umd", 50 | // name: "pinorama-client", 51 | // }, 52 | // ], 53 | // plugins: [ 54 | // alias({ 55 | // entries: [ 56 | // { find: "./platform/node.js", replacement: "./platform/browser.js" }, 57 | // ], 58 | // }), 59 | // resolve({ browser: true }), 60 | // commonjs(), 61 | // typescript(), 62 | // ], 63 | // external: ["zod"], 64 | // }, 65 | 66 | // // Browser CJS build 67 | // { 68 | // input: inputFile, 69 | // output: [ 70 | // { 71 | // file: `dist/browser/${outputFileName}.cjs.js`, 72 | // format: "cjs", 73 | // }, 74 | // ], 75 | // plugins: [ 76 | // alias({ 77 | // entries: [ 78 | // { find: "./platform/node.js", replacement: "./platform/browser.js" }, 79 | // ], 80 | // }), 81 | // resolve({ browser: true }), 82 | // commonjs(), 83 | // typescript(), 84 | // ], 85 | // external: ["zod"], 86 | // }, 87 | 88 | // Node.js ESM bundle 89 | { 90 | input: inputFile, 91 | output: [ 92 | { 93 | file: `dist/node/${outputFileName}.mjs`, 94 | format: "es", 95 | sourcemap: true 96 | } 97 | ], 98 | plugins: [resolve(), commonjs(), typescript()], 99 | external: ["zod"] 100 | }, 101 | 102 | // Node.js CJS build 103 | { 104 | input: inputFile, 105 | output: [ 106 | { 107 | file: `dist/node/${outputFileName}.cjs`, 108 | format: "cjs", 109 | sourcemap: true 110 | } 111 | ], 112 | plugins: [resolve(), commonjs(), typescript()], 113 | external: ["zod"] 114 | } 115 | ] 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pinorama 🌀 2 | 3 | ![Build Status](https://github.com/pinoramajs/pinorama/actions/workflows/turbo.yml/badge.svg?branch=main) 4 | [![Known Vulnerabilities](https://snyk.io/test/github/pinoramajs/pinorama/badge.svg)](https://snyk.io/test/github/pinoramajs/pinorama) 5 | [![All Contributors](https://img.shields.io/github/all-contributors/pinoramajs/pinorama?color=ee8449&style=flat-square)](#contributors-) 6 | 7 | ⚠️ **Warning: This project is currently under development and not ready for production use.** 8 | 9 | Pinorama is a suite of node.js packages that allow you to store and explore [pino](https://getpino.io) logs. It contains: 10 | 11 | - **server:** http server that stores logs using [orama](https://askorama.ai) and exposes rest api using [fastify](https://fastify.dev). 12 | - **client:** http client that streams and retrieves logs from the pinorama-server. 13 | - **transport:** pino logger transport that streams logs to the pinorama-server. 14 | - **docs:** project documentation available online at [pinorama.dev](https://pinorama.dev) powered by [vitepress](https://vitepress.dev). 15 | - **studio:** standalone app that allows you to explore the logs using [react](https://react.dev) and [shadcn/ui](https://ui.shadcn.com). 16 | 17 | --- 18 | 19 | This README will be updated as the project evolves. 20 | 21 | --- 22 | 23 | ## Contributors ✨ 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
Francesco Pasqua
Francesco Pasqua

💻 🎨 📖 🤔 🚧 📆 👀
Matteo Pietro Dazzi
Matteo Pietro Dazzi

💻 👀 🎨
36 | 37 | 38 | 39 | 40 | 41 | 42 | This project follows the [all-contributors](https://allcontributors.org/) specification. Contributions of any kind welcome! 43 | -------------------------------------------------------------------------------- /packages/pinorama-server/src/index.mts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs" 2 | import { create } from "@orama/orama" 3 | import { restoreFromFile } from "@orama/plugin-data-persistence/server" 4 | import Fastify from "fastify" 5 | import fp from "fastify-plugin" 6 | 7 | import { pino as defaultPreset } from "pinorama-presets" 8 | import * as plugins from "./plugins/index.mjs" 9 | import * as routes from "./routes/index.mjs" 10 | import { withPinoramaMetadataSchema } from "./utils/metadata.mjs" 11 | 12 | import type { AnyOrama, AnySchema } from "@orama/orama" 13 | import type { 14 | FastifyInstance, 15 | FastifyPluginAsync, 16 | FastifyRegisterOptions, 17 | FastifyServerOptions, 18 | LogLevel, 19 | RegisterOptions 20 | } from "fastify" 21 | import type { PinoramaIntrospection } from "pinorama-types" 22 | 23 | declare module "fastify" { 24 | interface FastifyInstance { 25 | pinoramaDb: AnyOrama 26 | pinoramaOpts: PinoramaServerOptions 27 | } 28 | } 29 | 30 | type PersistenceFormat = "json" | "dpack" | "binary" // orama does not export this type 31 | 32 | type PinoramaServerOptions = { 33 | adminSecret?: string 34 | dbSchema?: AnySchema 35 | dbPath?: string 36 | dbFormat?: PersistenceFormat 37 | prefix?: string 38 | logLevel?: LogLevel 39 | introspection: PinoramaIntrospection 40 | } 41 | 42 | export const defaultOptions: PinoramaServerOptions = { 43 | adminSecret: process.env.PINORAMA_SERVER_ADMIN_SECRET, 44 | dbFormat: "json", 45 | dbSchema: defaultPreset.schema, 46 | introspection: defaultPreset.introspection 47 | } 48 | 49 | const fastifyPinoramaServer: FastifyPluginAsync = async ( 50 | fastify, 51 | options 52 | ) => { 53 | const opts = { ...defaultOptions, ...options } 54 | 55 | const db = fs.existsSync(opts.dbPath as string) 56 | ? await restoreFromFile(opts.dbFormat, opts.dbPath) 57 | : create({ schema: withPinoramaMetadataSchema(opts.dbSchema) }) 58 | 59 | fastify.decorate("pinoramaOpts", opts) 60 | fastify.decorate("pinoramaDb", db) 61 | 62 | const registerOpts: RegisterOptions = {} 63 | 64 | if (opts.prefix) { 65 | registerOpts.prefix = opts.prefix 66 | } 67 | 68 | if (opts.logLevel) { 69 | registerOpts.logLevel = opts.logLevel 70 | } 71 | 72 | fastify.register(routes.bulkRoute, registerOpts) 73 | fastify.register(routes.introspectionRoute, registerOpts) 74 | fastify.register(routes.persistRoute, registerOpts) 75 | fastify.register(routes.searchRoute, registerOpts) 76 | fastify.register(routes.stylesRoute, registerOpts) 77 | 78 | fastify.register(plugins.gracefulSaveHook) 79 | fastify.register(plugins.authHook) 80 | } 81 | 82 | function createServer( 83 | pinoramaOptions: FastifyRegisterOptions, 84 | fastifyOptions?: FastifyServerOptions 85 | ): FastifyInstance { 86 | const fastify = Fastify(fastifyOptions) 87 | fastify.register(fastifyPinoramaServer, pinoramaOptions) 88 | return fastify 89 | } 90 | 91 | const plugin = fp(fastifyPinoramaServer, { 92 | fastify: "5.x", 93 | name: "fastify-pinorama-server" 94 | }) 95 | 96 | export default plugin 97 | export { fastifyPinoramaServer, createServer } 98 | -------------------------------------------------------------------------------- /packages/pinorama-transport/tests/integration.test.mts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it } from "vitest" 2 | 3 | import { setTimeout } from "node:timers/promises" 4 | import { pino } from "pino" 5 | import { PinoramaClient } from "pinorama-client" 6 | import { createServer } from "pinorama-server" 7 | import pinoramaTransport from "../src/lib.mjs" 8 | 9 | describe("pinoramaTransport", async () => { 10 | let pinoramaClientInstance: PinoramaClient 11 | let pinoramaServerUrl: string 12 | 13 | beforeEach(async () => { 14 | const pinoramaServerInstance = createServer() 15 | 16 | await pinoramaServerInstance.listen() 17 | await pinoramaServerInstance.ready() 18 | 19 | const address = pinoramaServerInstance.server.address() 20 | const port = typeof address === "string" ? address : address?.port 21 | 22 | pinoramaServerUrl = `http://localhost:${port}` 23 | pinoramaClientInstance = new PinoramaClient({ url: pinoramaServerUrl }) 24 | 25 | return async () => { 26 | await pinoramaServerInstance.close() 27 | } 28 | }) 29 | 30 | it("should store a log line to pinorama server", async () => { 31 | const transport = pinoramaTransport({ url: pinoramaServerUrl }) 32 | const log = pino(transport) 33 | 34 | // act 35 | log.info("hello world") 36 | 37 | setImmediate(() => transport.end()) 38 | await setTimeout(100) 39 | 40 | const response = await pinoramaClientInstance.search({}) 41 | expect(response.hits.length).toBe(1) 42 | expect(response.hits[0].document.msg).toBe("hello world") 43 | }) 44 | 45 | it("should store a deeply nested log line to pinorama server", async () => { 46 | const transport = pinoramaTransport({ url: pinoramaServerUrl }) 47 | const log = pino(transport) 48 | 49 | // act 50 | log.info({ 51 | deeply: { 52 | nested: { 53 | hello: "world" 54 | } 55 | } 56 | }) 57 | 58 | setImmediate(() => transport.end()) 59 | await setTimeout(100) 60 | 61 | const response = await pinoramaClientInstance.search({}) 62 | expect(response.hits.length).toBe(1) 63 | expect(response.hits[0].document.deeply.nested.hello).toBe("world") 64 | }) 65 | 66 | it("should store log lines in bulk", async () => { 67 | const transport = pinoramaTransport({ url: pinoramaServerUrl }) 68 | const log = pino(transport) 69 | 70 | // act 71 | log.info("hello world") 72 | log.info("hello world") 73 | log.info("hello world") 74 | log.info("hello world") 75 | log.info("hello world") 76 | 77 | setImmediate(() => transport.end()) 78 | await setTimeout(100) 79 | 80 | const response = await pinoramaClientInstance.search({}) 81 | expect(response.hits.length).toBe(5) 82 | for (const hit of response.hits) { 83 | expect(hit.document.msg).toBe("hello world") 84 | } 85 | }) 86 | 87 | it("should ignore all values except non-empty plain objects", async () => { 88 | const transport = pinoramaTransport({ url: pinoramaServerUrl }) 89 | 90 | // act 91 | transport.write("true\n") 92 | transport.write("null\n") 93 | transport.write("12\n") 94 | transport.write("{}\n") 95 | 96 | setImmediate(() => transport.end()) 97 | await setTimeout(100) 98 | 99 | const response = await pinoramaClientInstance.search({}) 100 | expect(response.hits.length).toBe(0) 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /packages/pinorama-transport/src/lib.mts: -------------------------------------------------------------------------------- 1 | import { setInterval } from "node:timers" 2 | import abstractTransport from "pino-abstract-transport" 3 | import { PinoramaClient } from "pinorama-client/node" 4 | 5 | import type { Transform } from "node:stream" 6 | import type { PinoramaClientOptions } from "pinorama-client/node" 7 | import type { BaseOramaPinorama, PinoramaDocument } from "pinorama-types" 8 | 9 | type BulkOptions = { 10 | batchSize: number 11 | flushInterval: number 12 | } 13 | 14 | export const defaultBulkOptions: BulkOptions = { 15 | batchSize: 100, 16 | flushInterval: 5000 17 | } 18 | 19 | export type PinoramaTransportOptions = PinoramaClientOptions & BulkOptions 20 | 21 | /** 22 | * Creates a pino transport that sends logs to a Pinorama server. 23 | * 24 | * @param {Partial} options - Optional settings overrides. 25 | * @returns {Transform} Configured transport instance. 26 | */ 27 | export default function pinoramaTransport( 28 | options: Partial 29 | ): Transform { 30 | const clientOpts = filterOptions(options, [ 31 | "url", 32 | "maxRetries", 33 | "backoff", 34 | "backoffFactor", 35 | "backoffMax", 36 | "adminSecret" 37 | ]) 38 | 39 | const bulkOpts = filterOptions(options, ["batchSize", "flushInterval"]) 40 | 41 | const client = new PinoramaClient(clientOpts) 42 | 43 | /* build */ 44 | const buildFn = async (stream: Transform) => { 45 | const buffer: PinoramaDocument[] = [] 46 | let flushing = false 47 | 48 | const opts: BulkOptions = { 49 | batchSize: bulkOpts?.batchSize ?? defaultBulkOptions.batchSize, 50 | flushInterval: bulkOpts?.flushInterval ?? defaultBulkOptions.flushInterval 51 | } 52 | 53 | const flush = async () => { 54 | if (buffer.length === 0 || flushing) return 55 | flushing = true 56 | 57 | try { 58 | stream.pause() 59 | await client.insert(buffer) 60 | buffer.length = 0 61 | } catch (error) { 62 | console.error("Failed to flush logs:", error) 63 | } finally { 64 | stream.resume() 65 | flushing = false 66 | } 67 | } 68 | 69 | const intervalId = setInterval(() => { 70 | flush() 71 | }, opts.flushInterval) 72 | 73 | stream.on("data", async (data) => { 74 | buffer.push(data) 75 | if (buffer.length >= opts.batchSize) { 76 | await flush() 77 | } 78 | }) 79 | 80 | stream.on("end", async () => { 81 | clearInterval(intervalId) 82 | await flush() 83 | }) 84 | } 85 | 86 | /* parseLine */ 87 | const parseLineFn = (line: string) => { 88 | const obj = JSON.parse(line) 89 | 90 | if (Object.prototype.toString.call(obj) !== "[object Object]") { 91 | throw new Error("not a plain object.") 92 | } 93 | 94 | if (Object.keys(obj).length === 0) { 95 | throw new Error("object is empty.") 96 | } 97 | 98 | return obj 99 | } 100 | 101 | return abstractTransport(buildFn, { parseLine: parseLineFn }) 102 | } 103 | 104 | /** 105 | * Filters and returns options specified by keys. 106 | */ 107 | export function filterOptions( 108 | options: Partial, 109 | keys: (keyof T)[] 110 | ): Partial | undefined { 111 | let result: Partial | undefined 112 | for (const key of keys) { 113 | if (key in options) { 114 | result = result || {} 115 | result[key] = options[key] 116 | } 117 | } 118 | return result 119 | } 120 | -------------------------------------------------------------------------------- /packages/pinorama-presets/src/presets/fastify.mts: -------------------------------------------------------------------------------- 1 | import { createPreset } from "../utils.mjs" 2 | 3 | export const fastify = createPreset( 4 | { 5 | time: "number", 6 | level: "enum", 7 | msg: "string", 8 | pid: "enum", 9 | hostname: "string", 10 | reqId: "string", 11 | req: { 12 | method: "string", 13 | url: "string", 14 | hostname: "string", 15 | remoteAddress: "string", 16 | remotePort: "enum" 17 | }, 18 | res: { 19 | statusCode: "enum" 20 | }, 21 | responseTime: "number" 22 | }, 23 | { 24 | facets: { 25 | level: "enum", 26 | msg: "string", 27 | "req.method": "string", 28 | "req.url": "string", 29 | "res.statusCode": "enum", 30 | "req.hostname": "string", 31 | "req.remoteAddress": "string", 32 | "req.remotePort": "enum", 33 | pid: "enum", 34 | hostname: "string" 35 | }, 36 | columns: { 37 | time: { visible: true, size: 150 }, // pino 38 | level: { visible: true, size: 70 }, // pino 39 | msg: { visible: true, size: 380 }, // pino 40 | reqId: { visible: true, size: 80 }, 41 | "req.method": { visible: true, size: 80 }, 42 | "req.url": { visible: true, size: 100 }, 43 | "res.statusCode": { visible: true, size: 70 }, 44 | "req.hostname": { visible: false, size: 150 }, 45 | "req.remoteAddress": { visible: false, size: 100 }, 46 | "req.remotePort": { visible: false, size: 80 }, 47 | responseTime: { visible: false, size: 150 }, 48 | pid: { visible: false, size: 70 }, // pino 49 | hostname: { visible: false, size: 150 } // pino 50 | }, 51 | labels: { 52 | level: [ 53 | "Level", 54 | { 55 | 10: "TRACE", 56 | 20: "DEBUG", 57 | 30: "INFO", 58 | 40: "WARN", 59 | 50: "ERROR", 60 | 60: "FATAL" 61 | } 62 | ], 63 | time: "Time", 64 | msg: "Message", 65 | pid: "PID", 66 | hostname: "Host", 67 | reqId: "Req. ID", 68 | "req.method": [ 69 | "Method", 70 | { 71 | GET: "GET", 72 | POST: "POST", 73 | PUT: "PUT", 74 | PATCH: "PATCH", 75 | DELETE: "DELETE", 76 | HEAD: "HEAD", 77 | OPTIONS: "OPTIONS" 78 | } 79 | ], 80 | "req.url": "URL", 81 | "req.hostname": "Req. Host", 82 | "req.remoteAddress": "Address", 83 | "req.remotePort": "Port", 84 | "res.statusCode": "Status", 85 | responseTime: "Res. Time" 86 | }, 87 | formatters: { 88 | time: "timestamp" 89 | }, 90 | styles: { 91 | time: { 92 | opacity: "0.5" 93 | }, 94 | level: [ 95 | {}, 96 | { 97 | 10: { color: "var(--color-gray-500)" }, 98 | 20: { color: "var(--color-purple-500)" }, 99 | 30: { color: "var(--color-lime-500)" }, 100 | 40: { color: "var(--color-yellow-500)" }, 101 | 50: { color: "var(--color-red-500)" }, 102 | 60: { color: "var(--color-red-500)" } 103 | } 104 | ], 105 | "req.method": [ 106 | {}, 107 | { 108 | GET: { color: "var(--color-blue-500)" }, 109 | POST: { color: "var(--color-green-500)" }, 110 | PUT: { color: "var(--color-yellow-500)" }, 111 | PATCH: { color: "var(--color-orange-500)" }, 112 | DELETE: { color: "var(--color-red-500)" }, 113 | HEAD: { color: "var(--color-purple-500)" }, 114 | OPTIONS: { color: "var(--color-cyan-500)" } 115 | } 116 | ] 117 | } 118 | } 119 | ) 120 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | // const Table = React.forwardRef< 6 | // HTMLTableElement, 7 | // React.HTMLAttributes 8 | // >(({ className, ...props }, ref) => ( 9 | //
10 | // 15 | // 16 | // )) 17 | const Table = React.forwardRef< 18 | HTMLTableElement, 19 | React.HTMLAttributes 20 | >(({ className, ...props }, ref) => ( 21 |
26 | )) 27 | Table.displayName = "Table" 28 | 29 | const TableHeader = React.forwardRef< 30 | HTMLTableSectionElement, 31 | React.HTMLAttributes 32 | >(({ className, ...props }, ref) => ( 33 | 34 | )) 35 | TableHeader.displayName = "TableHeader" 36 | 37 | const TableBody = React.forwardRef< 38 | HTMLTableSectionElement, 39 | React.HTMLAttributes 40 | >(({ className, ...props }, ref) => ( 41 | 46 | )) 47 | TableBody.displayName = "TableBody" 48 | 49 | const TableFooter = React.forwardRef< 50 | HTMLTableSectionElement, 51 | React.HTMLAttributes 52 | >(({ className, ...props }, ref) => ( 53 | tr]:last:border-b-0", 57 | className 58 | )} 59 | {...props} 60 | /> 61 | )) 62 | TableFooter.displayName = "TableFooter" 63 | 64 | const TableRow = React.forwardRef< 65 | HTMLTableRowElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 | 76 | )) 77 | TableRow.displayName = "TableRow" 78 | 79 | const TableHead = React.forwardRef< 80 | HTMLTableCellElement, 81 | React.ThHTMLAttributes 82 | >(({ className, ...props }, ref) => ( 83 |
91 | )) 92 | TableHead.displayName = "TableHead" 93 | 94 | const TableCell = React.forwardRef< 95 | HTMLTableCellElement, 96 | React.TdHTMLAttributes 97 | >(({ className, ...props }, ref) => ( 98 | 103 | )) 104 | TableCell.displayName = "TableCell" 105 | 106 | const TableCaption = React.forwardRef< 107 | HTMLTableCaptionElement, 108 | React.HTMLAttributes 109 | >(({ className, ...props }, ref) => ( 110 |
115 | )) 116 | TableCaption.displayName = "TableCaption" 117 | 118 | export { 119 | Table, 120 | TableHeader, 121 | TableBody, 122 | TableFooter, 123 | TableHead, 124 | TableRow, 125 | TableCell, 126 | TableCaption 127 | } 128 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/components/log-viewer/components/header/header.tsx: -------------------------------------------------------------------------------- 1 | import { SearchInput } from "@/components/search-input" 2 | import { useIntl } from "react-intl" 3 | import { ToggleLiveButton } from "./toggle-live-button" 4 | 5 | import { IconButton } from "@/components/icon-button/icon-button" 6 | import { useModuleHotkeys } from "@/hooks/use-module-hotkeys" 7 | import LogExplorerModule from "@/modules/log-explorer" 8 | import type { AnySchema } from "@orama/orama" 9 | import type { Table } from "@tanstack/react-table" 10 | import { 11 | FilterIcon, 12 | FilterXIcon, 13 | PanelRightIcon, 14 | RefreshCwIcon 15 | } from "lucide-react" 16 | import type { PinoramaIntrospection } from "pinorama-types" 17 | import { ToggleColumnsButton } from "./toggle-columns-button" 18 | 19 | type LogViewerHeaderProps = { 20 | searchInputRef: React.RefObject 21 | introspection: PinoramaIntrospection 22 | table: Table 23 | searchText: string 24 | showClearFiltersButton: boolean 25 | liveMode: boolean 26 | isLoading: boolean 27 | onSearchTextChange: (text: string) => void 28 | onToggleFiltersButtonClick: () => void 29 | onClearFiltersButtonClick: () => void 30 | onToggleLiveButtonClick: (live: boolean) => void 31 | onRefreshButtonClick: () => void 32 | onToggleDetailsButtonClick: () => void 33 | } 34 | 35 | export function LogViewerHeader(props: LogViewerHeaderProps) { 36 | const intl = useIntl() 37 | const moduleHotkeys = useModuleHotkeys(LogExplorerModule) 38 | 39 | const hotkeys = { 40 | showFilters: moduleHotkeys.getHotkey("showFilters"), 41 | refresh: moduleHotkeys.getHotkey("refresh"), 42 | clearFilters: moduleHotkeys.getHotkey("clearFilters"), 43 | showDetails: moduleHotkeys.getHotkey("showDetails") 44 | } 45 | 46 | return ( 47 |
48 | 55 | 62 | 66 | 74 | {props.showClearFiltersButton ? ( 75 | 82 | ) : null} 83 | 87 | 94 |
95 | ) 96 | } 97 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/title-bar/components/connection-status-button.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | 3 | import { Button } from "@/components/ui/button" 4 | import { 5 | Form, 6 | FormControl, 7 | FormField, 8 | FormItem, 9 | FormLabel, 10 | FormMessage 11 | } from "@/components/ui/form" 12 | import { Input } from "@/components/ui/input" 13 | import { 14 | Popover, 15 | PopoverContent, 16 | PopoverTrigger 17 | } from "@/components/ui/popover" 18 | import { useAppConfig } from "@/contexts" 19 | import { ConnectionStatus, usePinoramaConnection } from "@/hooks" 20 | import { zodResolver } from "@hookform/resolvers/zod" 21 | import { useForm } from "react-hook-form" 22 | import { FormattedMessage } from "react-intl" 23 | import { z } from "zod" 24 | 25 | const formSchema = z.object({ 26 | connectionUrl: z.string().url("Invalid URL") 27 | }) 28 | 29 | const STATUS_COLOR: Record = { 30 | [ConnectionStatus.Disconnected]: "bg-gray-500", 31 | [ConnectionStatus.Connecting]: "bg-orange-500", 32 | [ConnectionStatus.Connected]: "bg-green-500", 33 | [ConnectionStatus.Failed]: "bg-red-500", 34 | [ConnectionStatus.Unknown]: "bg-gray-500" 35 | } 36 | 37 | export function ConnectionStatusButton() { 38 | const appConfig = useAppConfig() 39 | const { connectionStatus } = usePinoramaConnection() 40 | 41 | const [open, setOpen] = useState(false) 42 | 43 | const form = useForm>({ 44 | resolver: zodResolver(formSchema), 45 | defaultValues: { 46 | connectionUrl: appConfig?.config.connectionUrl || "" 47 | } 48 | }) 49 | 50 | const onSubmit = (values: z.infer) => { 51 | appConfig?.setConfig({ 52 | ...appConfig.config, 53 | connectionUrl: values.connectionUrl 54 | }) 55 | 56 | setOpen(false) 57 | } 58 | 59 | return ( 60 | 61 | 62 | 77 | 78 | 79 |
80 | 81 | ( 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | )} 95 | /> 96 |
97 | 106 | 109 |
110 | 111 | 112 |
113 |
114 | ) 115 | } 116 | -------------------------------------------------------------------------------- /packages/pinorama-transport/src/cli.mts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import { readFileSync } from "node:fs" 4 | import type { Transform } from "node:stream" 5 | import { pipeline } from "node:stream/promises" 6 | import { fileURLToPath } from "node:url" 7 | import minimist from "minimist" 8 | import { defaultClientOptions } from "pinorama-client" 9 | import pinoramaTransport, { defaultBulkOptions } from "./lib.mjs" 10 | import type { PinoramaTransportOptions } from "./lib.mjs" 11 | 12 | type PinoramaCliOptions = PinoramaTransportOptions & { 13 | version: string 14 | help: string 15 | } 16 | 17 | /** 18 | * Entry function to start the CLI application. 19 | * 20 | * @param {Partial} argv - The set of CLI options. 21 | */ 22 | async function main(argv: Partial) { 23 | const pj = fileURLToPath(new URL("../package.json", import.meta.url)) 24 | const { version } = JSON.parse(readFileSync(pj, "utf8")) 25 | 26 | if (argv.help) { 27 | console.log(` 28 | pino-pinorama v${version} 29 | 30 | Description: 31 | A tool for send pino logs to Pinorama server. 32 | 33 | Usage: 34 | pino-pinorama [options] 35 | 36 | Options: 37 | -h, --help Display this help message and exit. 38 | -v, --version Show application version. 39 | -u, --url Set Pinorama server URL. 40 | -k, --adminSecret Secret key for authentication. 41 | -b, --batchSize Define logs per bulk insert (default: ${defaultBulkOptions.batchSize}). 42 | -f, --flushInterval Set flush wait time in ms (default: ${defaultBulkOptions.flushInterval}). 43 | -m, --maxRetries Max retry attempts for requests (default: ${defaultClientOptions.maxRetries}). 44 | -i, --backoff Initial backoff time in ms for retries (default: ${defaultClientOptions.backoff}). 45 | -d, --backoffFactor Backoff factor for exponential increase (default: ${defaultClientOptions.backoffFactor}). 46 | -x, --backoffMax Maximum backoff time in ms (default: ${defaultClientOptions.backoffMax}). 47 | 48 | Example: 49 | cat logs | pino-pinorama --url http://localhost:6200 50 | `) 51 | process.exit() 52 | } 53 | 54 | if (argv.version) { 55 | console.log(version) 56 | process.exit() 57 | } 58 | 59 | if (!argv.url) { 60 | console.error( 61 | "Error: missing required argument '--url'.\n" + 62 | "For more information, use the '--help' argument." 63 | ) 64 | process.exit(1) 65 | } 66 | 67 | let transport: Transform 68 | 69 | try { 70 | transport = pinoramaTransport({ 71 | url: argv.url, 72 | adminSecret: argv.adminSecret, 73 | batchSize: argv.batchSize, 74 | flushInterval: argv.flushInterval, 75 | maxRetries: argv.maxRetries, 76 | backoff: argv.backoff, 77 | backoffFactor: argv.backoffFactor, 78 | backoffMax: argv.backoffMax 79 | }) 80 | } catch (e: any) { 81 | console.error(`Error: ${e.message}`) 82 | process.exit(1) 83 | } 84 | 85 | transport.on("error", (error) => { 86 | console.error("pinoramaTransport error:", error) 87 | }) 88 | 89 | pipeline(process.stdin, transport) 90 | } 91 | 92 | const cliOptions = minimist(process.argv.slice(2), { 93 | alias: { 94 | v: "version", 95 | h: "help", 96 | k: "adminSecret", 97 | u: "url", 98 | b: "batchSize", 99 | f: "flushInterval", 100 | m: "maxRetries", 101 | i: "backoff", 102 | d: "backoffFactor", 103 | x: "backoffMax" 104 | }, 105 | default: { 106 | batchSize: defaultBulkOptions.batchSize, 107 | flushInterval: defaultBulkOptions.flushInterval, 108 | maxRetries: defaultClientOptions.maxRetries, 109 | backoff: defaultClientOptions.backoff, 110 | backoffFactor: defaultClientOptions.backoffFactor, 111 | backoffMax: defaultClientOptions.backoffMax 112 | }, 113 | boolean: ["version", "help"], 114 | string: ["adminSecret", "url"] 115 | }) 116 | 117 | main(cliOptions) 118 | -------------------------------------------------------------------------------- /packages/pinorama-docs/guide/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | # Overview 6 | 7 | **Pinorama** is a suite of tools designed to help store, explore, and analyze logs from Node.js applications, specifically those generated by [Pino Logger](http://getpino.io/). It's built for development environments, offering flexible options for both real-time and post-mortem log analysis. 8 | 9 | ::: tip 10 | **Need a Quick Start?** 🚀 11 | 12 | If you're looking to get started quickly, check out [Pinorama Studio Quickstart](/guide/quick-start/), a Web App and CLI that allows you to store, view, filter, and analyze logs with minimal setup. 13 | ::: 14 | 15 | ## Understanding the suite 16 | 17 | Pinorama is made up of 4 key components, each with a specific role: 18 | 19 | ### 1. Pinorama Studio ⭐ 20 | 21 | If you’re looking to get started quickly, Pinorama Studio is the tool for you. It’s a web app and CLI that allows you to store, view, filter, and analyze logs with minimal setup. It works only with Pino structured JSON format logs. Studio can also function as a server when launched with the `--server` option, making it a ideal for scenarios where you want to both receive and view logs in real-time like development environments or debugging sessions. 22 | 23 | ### 2. Pinorama Client 24 | 25 | An isomorphic library that works in both Node.js and the Browser. It’s essentially an HTTP client that mirrors [Orama Search’s API](https://docs.orama.com/open-source/usage/search/introduction), allowing you to interact with an Orama instance through Pinorama Server. 26 | However, this tool is typically used internally by Studio and Transport, so you probably won’t need to interact with it directly unless you’re building a custom solution. 27 | 28 | ### 3. Pinorama Server 29 | 30 | A web server built on Fastify that exposes RESTful endpoints for storing and retrieving logs via [Orama](https://askorama.ai/). It's ideal for cases where you need to index logs with custom structures, customize log line styles (such as colors and formatters), or set up a server to collect and expose logs for real-time access through `pinorama-studio`. Pinorama Server can be deployed as a standalone application or integrated into existing projects as a Fastify plugin. 31 | 32 | ### 4. Pinorama Transport 33 | 34 | Pinorama Transport is used primarily as a [transport](https://getpino.io/#/docs/transports) layer for Pino Logger. It can be integrated programmatically into your Node.js applications to stream logs to a Pinorama Server, or used via CLI to send logs from the terminal using piped output. This flexibility makes it ideal for both automated logging and manual log transmission from processes. 35 | 36 | ## How it works 37 | 38 | Here’s a quick look at how the different components of Pinorama fit together: 39 | 40 | 1. **Generate Logs (Pino Logger):** 41 | - **Pino Logger** creates logs in a structured JSON format as your Node.js application runs. These logs capture essential events and data, forming the basis for further processing and analysis. 42 | 43 | 2. **Stream Logs (Pinorama Transport with Pinorama Client):** 44 | - **Pinorama Transport** streams these logs to **Pinorama Server**. This can be done programmatically within your application or via CLI using piped output. **Pinorama Client** is used internally by Transport to send HTTP requests to **Pinorama Server**'s RESTful endpoints. 45 | 46 | 3. **Store and Index Logs (Pinorama Server):** 47 | - **Pinorama Server** receives the logs from **Pinorama Transport**. It stores and indexes these logs in memory using **Orama Search**, making them available for retrieval and analysis through its RESTful API. 48 | 49 | 4. **Explore Logs (Pinorama Studio with Pinorama Client):** 50 | - **Pinorama Studio** allows you to explore the stored logs through a web-based user interface. It connects to **Pinorama Server** using **Pinorama Client**, enabling real-time log viewing, filtering, and analysis. 51 | 52 | ## When to use 53 | 54 | ::: tip 55 | **For Development:** Pinorama is perfect for local development, offering tools for both live log streaming and detailed post-mortem analysis. 56 | ::: 57 | 58 | ::: danger 59 | **Avoid Production!** Pinorama Server is not recommended for production environments due to its in-memory log storage. 60 | ::: 61 | -------------------------------------------------------------------------------- /packages/pinorama-docs/.vitepress/config/en.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitepress" 2 | import { version } from "../../../pinorama-studio/package.json" 3 | 4 | export const en = defineConfig({ 5 | lang: "en-US", 6 | 7 | themeConfig: { 8 | nav: [ 9 | { 10 | text: "Guide", 11 | link: "/guide/", 12 | activeMatch: "/guide/" 13 | }, 14 | { 15 | text: "Studio", 16 | link: "/guide/pinorama-studio/", 17 | activeMatch: "/guide/pinorama-studio/" 18 | }, 19 | { 20 | text: "Client", 21 | link: "/guide/pinorama-client/", 22 | activeMatch: "/guide/pinorama-client/" 23 | }, 24 | { 25 | text: "Server", 26 | link: "/guide/pinorama-server/", 27 | activeMatch: "/guide/pinorama-server/" 28 | }, 29 | { 30 | text: "Transport", 31 | link: "/guide/pinorama-transport/", 32 | activeMatch: "/guide/pinorama-transport/" 33 | }, 34 | { 35 | text: `v${version}`, 36 | items: [ 37 | { 38 | text: `v${version} (studio)`, 39 | link: "/" 40 | }, 41 | { 42 | text: "Release Notes", 43 | link: "https://github.com/pinoramajs/pinorama/releases" 44 | } 45 | ] 46 | } 47 | ], 48 | 49 | sidebar: [ 50 | { 51 | text: "Guide", 52 | items: [ 53 | { 54 | text: "Overview", 55 | link: "/guide/" 56 | }, 57 | { 58 | text: "Quick Start", 59 | link: "/guide/quick-start" 60 | }, 61 | { 62 | text: "Use Cases", 63 | link: "/guide/use-cases" 64 | } 65 | ] 66 | }, 67 | { 68 | text: "Studio ⭐", 69 | items: [ 70 | { 71 | text: "Overview", 72 | link: "/pinorama-studio" 73 | }, 74 | { 75 | text: "Installation", 76 | link: "/pinorama-studio/installation" 77 | }, 78 | { 79 | text: "Usage", 80 | link: "/pinorama-studio/usage" 81 | }, 82 | { 83 | text: "CLI", 84 | link: "/pinorama-studio/api" 85 | } 86 | ] 87 | }, 88 | { 89 | text: "Client", 90 | items: [ 91 | { 92 | text: "Overview", 93 | link: "/pinorama-client" 94 | }, 95 | { 96 | text: "Installation", 97 | link: "/pinorama-client/installation" 98 | }, 99 | { 100 | text: "Usage", 101 | link: "/pinorama-client/usage" 102 | }, 103 | { 104 | text: "API Reference", 105 | link: "/pinorama-client/api" 106 | } 107 | ] 108 | }, 109 | { 110 | text: "Server", 111 | items: [ 112 | { 113 | text: "Overview", 114 | link: "/pinorama-server" 115 | }, 116 | { 117 | text: "Installation", 118 | link: "/pinorama-server/installation" 119 | }, 120 | { 121 | text: "Configuration", 122 | link: "/pinorama-server/configuration" 123 | }, 124 | { 125 | text: "API Reference", 126 | link: "/pinorama-server/api" 127 | } 128 | ] 129 | }, 130 | { 131 | text: "Transport", 132 | items: [ 133 | { 134 | text: "Overview", 135 | link: "/pinorama-transport" 136 | }, 137 | { 138 | text: "Installation", 139 | link: "/pinorama-transport/installation" 140 | }, 141 | { 142 | text: "Usage", 143 | link: "/pinorama-transport/usage" 144 | } 145 | ] 146 | } 147 | ], 148 | 149 | footer: { 150 | message: "Open Source ❤ MIT Licensed", 151 | copyright: "© 2024 Francesco Pasqua & Contributors" 152 | }, 153 | 154 | docFooter: { 155 | prev: "Previous page", 156 | next: "Next page" 157 | } 158 | } 159 | }) 160 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/modules/log-explorer/components/log-filters/components/facet.tsx: -------------------------------------------------------------------------------- 1 | import { EmptyStateInline } from "@/components/empty-state/empty-state" 2 | import { ErrorState } from "@/components/error-state/error-state" 3 | import { useCallback, useMemo, useState } from "react" 4 | import { useIntl } from "react-intl" 5 | import { useFacet } from "../hooks/use-facet" 6 | import { facetFilterOperationsFactory } from "../lib/operations" 7 | import { FacetBody } from "./facet-body" 8 | import { FacetHeader } from "./facet-header" 9 | 10 | import type { AnySchema } from "@orama/orama" 11 | import type { IntrospectionFacet, PinoramaIntrospection } from "pinorama-types" 12 | import type { FacetFilter, FacetValue, SearchFilters } from "../types" 13 | 14 | type FacetProps = { 15 | introspection: PinoramaIntrospection 16 | name: string 17 | type: IntrospectionFacet 18 | searchText: string 19 | filters: SearchFilters 20 | liveMode: boolean 21 | onFiltersChange: (filters: SearchFilters) => void 22 | } 23 | 24 | export function Facet(props: FacetProps) { 25 | const intl = useIntl() 26 | 27 | const [open, setOpen] = useState(true) 28 | 29 | const { 30 | data: facet, 31 | fetchStatus, 32 | status, 33 | error 34 | } = useFacet(props.name, props.searchText, props.filters, props.liveMode) 35 | 36 | const operations: any = facetFilterOperationsFactory(props.type) 37 | const criteria = props.filters[props.name] || operations.create() 38 | const selelectedOptionCount = operations.length(criteria) 39 | 40 | const handleReset = useCallback( 41 | (event: React.MouseEvent) => { 42 | event.stopPropagation() 43 | const filters = { ...props.filters } 44 | delete filters[props.name] 45 | props.onFiltersChange(filters) 46 | }, 47 | [props.onFiltersChange, props.name, props.filters] 48 | ) 49 | 50 | const selectedValuesNotInDataSource = useMemo(() => { 51 | const selectedItems: FacetValue[] = operations 52 | .values(props.filters[props.name] as FacetFilter) 53 | .map((value: string | number) => { 54 | return { 55 | value, 56 | count: facet?.values[value] || 0 57 | } 58 | }) 59 | 60 | return selectedItems.filter( 61 | (item) => !(item.value in (facet?.values || {})) 62 | ) 63 | }, [props.filters, props.name, facet?.values, operations]) 64 | 65 | const allValues = useMemo(() => { 66 | const currentValues = Object.entries(facet?.values || {}) 67 | .filter(([value]) => value !== "") // NOTE: Don't show empty values 68 | .map(([value, count]) => { 69 | // NOTE: If the value is a number of type string, 70 | // we need to parse it to a number. 71 | let parsedValue: string | number = value 72 | if (props.type === "enum" && Number.isFinite(+value)) { 73 | parsedValue = Number(value) 74 | } 75 | return { value: parsedValue, count } 76 | }) 77 | 78 | return [...selectedValuesNotInDataSource, ...currentValues] 79 | }, [selectedValuesNotInDataSource, facet?.values, props.type]) 80 | 81 | const hasError = status === "error" 82 | const hasNoData = allValues.length === 0 83 | 84 | return ( 85 |
86 | setOpen((value) => !value)} 93 | onCountClick={handleReset} 94 | /> 95 | {open ? ( 96 | hasError ? ( 97 | 98 | ) : hasNoData ? ( 99 | 103 | ) : ( 104 | 112 | ) 113 | ) : null} 114 |
115 | ) 116 | } 117 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as DialogPrimitive from "@radix-ui/react-dialog" 2 | import { X } from "lucide-react" 3 | import * as React from "react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Dialog = DialogPrimitive.Root 8 | 9 | const DialogTrigger = DialogPrimitive.Trigger 10 | 11 | const DialogPortal = DialogPrimitive.Portal 12 | 13 | const DialogClose = DialogPrimitive.Close 14 | 15 | const DialogOverlay = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => ( 19 | 27 | )) 28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 29 | 30 | const DialogContent = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, children, ...props }, ref) => ( 34 | 35 | 36 | 44 | {children} 45 | 46 | 47 | Close 48 | 49 | 50 | 51 | )) 52 | DialogContent.displayName = DialogPrimitive.Content.displayName 53 | 54 | const DialogHeader = ({ 55 | className, 56 | ...props 57 | }: React.HTMLAttributes) => ( 58 |
65 | ) 66 | DialogHeader.displayName = "DialogHeader" 67 | 68 | const DialogFooter = ({ 69 | className, 70 | ...props 71 | }: React.HTMLAttributes) => ( 72 |
79 | ) 80 | DialogFooter.displayName = "DialogFooter" 81 | 82 | const DialogTitle = React.forwardRef< 83 | React.ElementRef, 84 | React.ComponentPropsWithoutRef 85 | >(({ className, ...props }, ref) => ( 86 | 94 | )) 95 | DialogTitle.displayName = DialogPrimitive.Title.displayName 96 | 97 | const DialogDescription = React.forwardRef< 98 | React.ElementRef, 99 | React.ComponentPropsWithoutRef 100 | >(({ className, ...props }, ref) => ( 101 | 106 | )) 107 | DialogDescription.displayName = DialogPrimitive.Description.displayName 108 | 109 | export { 110 | Dialog, 111 | DialogPortal, 112 | DialogOverlay, 113 | DialogClose, 114 | DialogTrigger, 115 | DialogContent, 116 | DialogHeader, 117 | DialogFooter, 118 | DialogTitle, 119 | DialogDescription 120 | } 121 | -------------------------------------------------------------------------------- /packages/pinorama-studio/src/components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import type * as LabelPrimitive from "@radix-ui/react-label" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import * as React from "react" 4 | import { 5 | Controller, 6 | type ControllerProps, 7 | type FieldPath, 8 | type FieldValues, 9 | FormProvider, 10 | useFormContext 11 | } from "react-hook-form" 12 | 13 | import { Label } from "@/components/ui/label" 14 | import { cn } from "@/lib/utils" 15 | 16 | const Form = FormProvider 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath = FieldPath 21 | > = { 22 | name: TName 23 | } 24 | 25 | const FormFieldContext = React.createContext( 26 | {} as FormFieldContextValue 27 | ) 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath = FieldPath 32 | >({ 33 | ...props 34 | }: ControllerProps) => { 35 | return ( 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext) 44 | const itemContext = React.useContext(FormItemContext) 45 | const { getFieldState, formState } = useFormContext() 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState) 48 | 49 | if (!fieldContext) { 50 | throw new Error("useFormField should be used within ") 51 | } 52 | 53 | const { id } = itemContext 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState 62 | } 63 | } 64 | 65 | type FormItemContextValue = { 66 | id: string 67 | } 68 | 69 | const FormItemContext = React.createContext( 70 | {} as FormItemContextValue 71 | ) 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId() 78 | 79 | return ( 80 | 81 |
82 | 83 | ) 84 | }) 85 | FormItem.displayName = "FormItem" 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField() 92 | 93 | return ( 94 |