diff --git a/.gitignore b/.gitignore index 7ad289b..9874a71 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ record.log.* # Log files *.log +docs/blog diff --git a/collector/src/main.rs b/collector/src/main.rs index d356d88..6198622 100644 --- a/collector/src/main.rs +++ b/collector/src/main.rs @@ -870,7 +870,7 @@ async fn start_web_server_if_enabled( return Ok(None); } - let addr = format!("127.0.0.1:{}", port).parse() + let addr = format!("0.0.0.0:{}", port).parse() .map_err(|e| format!("Invalid server address: {}", e))?; let web_server = WebServer::new(event_sender, log_file).map_err(|e| format!("Failed to create web server: {}", e))?; diff --git a/collector/src/server/assets.rs b/collector/src/server/assets.rs index f9bfd13..9af360f 100644 --- a/collector/src/server/assets.rs +++ b/collector/src/server/assets.rs @@ -3,7 +3,7 @@ use rust_embed::RustEmbed; use std::borrow::Cow; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::fs; use mime_guess::from_path; @@ -12,14 +12,29 @@ use mime_guess::from_path; pub struct FrontendDist; pub struct FrontendAssets { - temp_dir: PathBuf, + serve_dir: PathBuf, + /// Whether we own the directory (temp extraction) and should clean it up on drop. + owned: bool, } impl FrontendAssets { pub fn new() -> Result> { + // Dev mode: serve directly from a disk directory if env var is set + if let Ok(dist_path) = std::env::var("AGENTSIGHT_FRONTEND_DIST") { + let dir = PathBuf::from(&dist_path); + if !dir.join("index.html").exists() { + return Err(format!( + "AGENTSIGHT_FRONTEND_DIST={} does not contain index.html", + dist_path + ).into()); + } + log::info!("📁 Dev mode: serving frontend from disk: {}", dir.display()); + return Ok(Self { serve_dir: dir, owned: false }); + } + let temp_dir = std::env::temp_dir().join(format!("agentsight-frontend-{}", uuid::Uuid::new_v4())); fs::create_dir_all(&temp_dir)?; - + // Extract all embedded assets to temp directory for file_path in FrontendDist::iter() { if let Some(content) = FrontendDist::get(&file_path) { @@ -30,23 +45,23 @@ impl FrontendAssets { fs::write(&full_path, &content.data)?; } } - + log::info!("📁 Extracted frontend assets to: {}", temp_dir.display()); - Ok(Self { temp_dir }) + Ok(Self { serve_dir: temp_dir, owned: true }) } - /// Get any asset by path from the extracted temp directory + /// Get any asset by path from the serve directory pub fn get(&self, path: &str) -> Option> { // Handle root path let file_path = if path == "/" || path == "/index.html" { - self.temp_dir.join("index.html") + self.serve_dir.join("index.html") } else { // Remove leading slash for file lookup let normalized_path = path.strip_prefix('/').unwrap_or(path); - self.temp_dir.join(normalized_path) + self.serve_dir.join(normalized_path) }; - // Try to read from temp directory + // Try to read from serve directory if let Ok(content) = fs::read(&file_path) { Some(Cow::Owned(content)) } else { @@ -68,20 +83,48 @@ impl FrontendAssets { from_path(file_path).first_or_octet_stream().to_string() } - /// List all available assets from the embedded dist + /// List all available assets pub fn list_all_assets(&self) -> Vec { - FrontendDist::iter().map(|s| s.to_string()).collect() + if self.owned { + // Embedded mode: use RustEmbed iterator + FrontendDist::iter().map(|s| s.to_string()).collect() + } else { + // Dev mode: walk the disk directory + let mut files = Vec::new(); + if let Ok(entries) = walkdir(&self.serve_dir, &self.serve_dir) { + files = entries; + } + files + } } } impl Drop for FrontendAssets { fn drop(&mut self) { - if self.temp_dir.exists() { - if let Err(e) = fs::remove_dir_all(&self.temp_dir) { - log::warn!("Failed to cleanup temp directory {}: {}", self.temp_dir.display(), e); + if !self.owned { + return; + } + if self.serve_dir.exists() { + if let Err(e) = fs::remove_dir_all(&self.serve_dir) { + log::warn!("Failed to cleanup temp directory {}: {}", self.serve_dir.display(), e); } else { - log::info!("🧹 Cleaned up temp directory: {}", self.temp_dir.display()); + log::info!("🧹 Cleaned up temp directory: {}", self.serve_dir.display()); } } } +} + +/// Recursively list files under `dir`, returning paths relative to `root`. +fn walkdir(dir: &Path, root: &Path) -> std::io::Result> { + let mut result = Vec::new(); + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + result.extend(walkdir(&path, root)?); + } else if let Ok(rel) = path.strip_prefix(root) { + result.push(rel.to_string_lossy().into_owned()); + } + } + Ok(result) } \ No newline at end of file diff --git a/docs/development.md b/docs/development.md new file mode 100755 index 0000000..4ceebb7 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,38 @@ +# Development Guide + +**English** | [中文](development.zh-CN.md) + +## Frontend Dev Mode (Disk-First Assets) + +The collector binary embeds frontend assets via `RustEmbed` at compile time. By default, every frontend change requires recompiling the collector (`cargo build --release`) to take effect. + +To speed up frontend development, set the `AGENTSIGHT_FRONTEND_DIST` environment variable to serve assets directly from disk. This way you only need to rebuild the frontend and restart the collector — no Rust recompilation needed. + +### Usage + +```sh +# 1. Build the frontend +make build-frontend + +# 2. Start the collector with disk-based frontend assets +AGENTSIGHT_FRONTEND_DIST=./frontend/dist sudo -E ./target/release/agentsight record -c claude --binary-path +``` + +After each frontend change: + +```sh +make build-frontend +# Restart the collector — changes take effect immediately, no cargo build needed +``` + +### How it works + +- On startup, the collector checks for the `AGENTSIGHT_FRONTEND_DIST` environment variable. +- **Set** — serves files directly from the specified directory, skipping the embedded asset extraction. The directory must contain `index.html`. +- **Not set** — falls back to the default behavior: extracts `RustEmbed` assets to a temp directory and cleans up on exit. + +### Notes + +- Use `sudo -E` to preserve the environment variable when running with sudo. +- The path can be relative (e.g., `./frontend/dist`) or absolute. +- In production, do not set this variable — the embedded assets will be used as usual. diff --git a/docs/development.zh-CN.md b/docs/development.zh-CN.md new file mode 100755 index 0000000..e4c568f --- /dev/null +++ b/docs/development.zh-CN.md @@ -0,0 +1,38 @@ +# 开发指南 + +[English](development.md) | **中文** + +## 前端开发模式(磁盘优先加载) + +collector 二进制在编译时通过 `RustEmbed` 将前端资源内嵌。默认情况下,每次前端改动都需要重新编译 collector(`cargo build --release`)才能生效。 + +为加速前端开发,可设置 `AGENTSIGHT_FRONTEND_DIST` 环境变量,让 collector 直接从磁盘目录读取前端资源。这样只需重新构建前端并重启 collector,无需重新编译 Rust 代码。 + +### 使用方法 + +```sh +# 1. 构建前端 +make build-frontend + +# 2. 设置环境变量启动 collector +AGENTSIGHT_FRONTEND_DIST=./frontend/dist sudo -E ./collector/target/release/agentsight record -c claude --binary-path /opt/node-v22.20.0/bin/node +``` + +之后每次修改前端: + +```sh +make build-frontend +# 重启 collector 即可生效,无需 cargo build +``` + +### 工作原理 + +- collector 启动时检查 `AGENTSIGHT_FRONTEND_DIST` 环境变量。 +- **已设置** — 直接从指定目录读取文件,跳过内嵌资源解压流程。目录中必须包含 `index.html`。 +- **未设置** — 使用默认行为:将 `RustEmbed` 内嵌资源解压到临时目录,退出时自动清理。 + +### 注意事项 + +- 使用 `sudo -E` 以在 sudo 下保留环境变量。 +- 路径支持相对路径(如 `./frontend/dist`)和绝对路径。 +- 生产环境中不要设置此变量,将正常使用内嵌资源。 diff --git a/docs/usage.md b/docs/usage.md index 7da7aaa..e662e30 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -2,9 +2,49 @@ **English** | [中文](usage.zh-CN.md) +## Building from Source + +### 1. Clone the repository and initialize submodules + +```sh +git clone https://github.com/eunomia-bpf/agentsight.git +cd agentsight +git submodule update --init --recursive +``` + +If you have already cloned the repository but the submodule directories (`libbpf/` and `bpftool/`) are empty, run: + +```sh +git submodule update --init --recursive +``` + +### 2. Install system dependencies + +```sh +make install +``` + +This installs the required build dependencies: libelf, zlib, clang, llvm, Node.js, and the Rust toolchain. + +### 3. Build + +```sh +make build +``` + +After a successful build, the agentsight binary is located at `collector/target/release/agentsight`. + +You can also build individual components: + +```sh +make build-bpf # eBPF C programs only +make build-rust # Rust collector only +make build-frontend # Frontend only +``` + ## Command-line parameters for monitoring Claude Code with agentsight -After successfully compiling from source, the agentsight binary is located in the `collector/target/release/` directory under the project root. Navigate to the source code root directory and run the following commands to test: +Navigate to the source code root directory and run the following commands to test: ```sh sudo ./collector/target/release/agentsight ssl --http-parser --http-filter "request.path_prefix=/v1/rgstr | response.status_code=202 | request.method=HEAD | response.body=" --ssl-filter "data=0\r\n\r\n" diff --git a/docs/usage.zh-CN.md b/docs/usage.zh-CN.md index ea88602..36c9c55 100644 --- a/docs/usage.zh-CN.md +++ b/docs/usage.zh-CN.md @@ -2,9 +2,49 @@ [English](usage.md) | **中文** +## 从源代码编译 + +### 1. 克隆仓库并初始化子模块 + +```sh +git clone https://github.com/eunomia-bpf/agentsight.git +cd agentsight +git submodule update --init --recursive +``` + +如果你已经克隆过仓库但尚未初始化子模块(`libbpf/` 和 `bpftool/` 目录为空),请执行: + +```sh +git submodule update --init --recursive +``` + +### 2. 安装系统依赖 + +```sh +make install +``` + +这会安装编译所需的 libelf、zlib、clang、llvm、Node.js 和 Rust 工具链。 + +### 3. 编译 + +```sh +make build +``` + +编译成功后,agentsight 二进制程序生成在 `collector/target/release/agentsight`。 + +也可以单独编译各组件: + +```sh +make build-bpf # 仅编译 eBPF C 程序 +make build-rust # 仅编译 Rust collector +make build-frontend # 仅编译前端 +``` + ## 使用agentsight监测Claude Code的命令行参数 -使用源代码编译成功后,agentsight二进制程序生成位置在当前目录下的collector/target/release/目录下,进入源代码的根目录,然后执行如下命令来测试: +进入源代码的根目录,然后执行如下命令来测试: ```sh sudo ./collector/target/release/agentsight ssl --http-parser --http-filter "request.path_prefix=/v1/rgstr | response.status_code=202 | request.method=HEAD | response.body=" --ssl-filter "data=0\r\n\r\n" @@ -17,3 +57,53 @@ sudo ./collector/target/release/agentsight agent -c "claude" --http-parser --htt ```sh sudo ./collector/target/release/agentsight agent -c claude --http-filter "request.path_prefix=/v1/rgstr | response.status_code=202 | request.method=HEAD | response.body=" --ssl-filter "data=0\r\n\r\n|data.type=binary" ``` + +## record 与 trace 子命令对比 + +agentsight 提供 `record` 和 `trace` 两个子命令,它们共用同一个底层执行逻辑,但面向不同的使用场景。 + +### record — 开箱即用的智能体录制 + +适用于快速录制 AI 智能体(Claude Code、Python AI 工具等)的行为,无需关心细节配置。 + +- `-c/--comm` 是**必填**参数,如 `-c claude` +- **自动开启**:SSL 监控 + 进程监控 + 系统监控 + Web 服务器(端口 7395) +- **内置过滤规则**:自动过滤掉注册请求(`/v1/rgstr`)、HEAD 请求、空响应体、202 状态码、二进制数据等噪音 +- 默认**静默模式**(不输出到控制台),数据写入 `record.log` +- 默认开启**日志轮转** + +典型用法: + +```sh +sudo ./agentsight record -c claude --binary-path +``` + +### trace — 完全可控的灵活监控 + +适用于需要自定义监控范围、过滤规则的调试和分析场景。 + +- **无必填参数**,所有功能独立开关 +- SSL(`--ssl`)、进程(`--process`)默认开启,但可关闭 +- 系统监控(`--system`)、stdio 捕获(`--stdio`)、Web 服务器(`--server`)默认**关闭**,需手动开启 +- 过滤规则完全由用户通过 `--ssl-filter`、`--http-filter` 自定义 +- 默认输出到控制台,可用 `-q` 静默 + +典型用法: + +```sh +sudo ./agentsight trace --ssl true --process false --server true --http-filter "request.method=POST" +``` + +### 对比总结 + +| 维度 | record | trace | +|------|--------|-------| +| 定位 | 一键录制,预设优化 | 灵活定制,精细控制 | +| 必填参数 | `-c ` | 无 | +| Web 服务器 | 始终开启 | 需 `--server true` | +| 系统监控 | 始终开启 | 需 `--system true` | +| 控制台输出 | 默认关闭 | 默认开启 | +| 过滤规则 | 内置预设 | 用户自定义 | +| 日志轮转 | 默认开启 | 需 `--rotate-logs` | + +简单来说:**日常录制用 `record`,深度调试用 `trace`**。 diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 937497c..e2ca459 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -3,6 +3,7 @@ import type { Metadata } from 'next' import './globals.css' +import { I18nProvider } from '@/i18n' export const metadata: Metadata = { title: 'Agent Tracer Frontend', @@ -17,7 +18,9 @@ export default function RootLayout({ return ( - {children} + + {children} + ) diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 69a87c4..38816ff 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -9,11 +9,14 @@ import { TimelineView } from '@/components/TimelineView'; import { ProcessTreeView } from '@/components/ProcessTreeView'; import { ResourceMetricsView } from '@/components/ResourceMetricsView'; import { UploadPanel } from '@/components/UploadPanel'; +import { LanguageSwitcher } from '@/components/common/LanguageSwitcher'; +import { useTranslation } from '@/i18n'; import { Event } from '@/types/event'; type ViewMode = 'log' | 'timeline' | 'process-tree' | 'metrics'; export default function Home() { + const { t } = useTranslation(); const [file, setFile] = useState(null); const [logContent, setLogContent] = useState(''); const [events, setEvents] = useState([]); @@ -173,11 +176,14 @@ export default function Home() {
{/* Header */}
+
+ +

- AgentSight Analyzer + {t('app.title')}

- Upload and analyze eBPF agent trace logs with dual view modes + {t('app.subtitle')}

@@ -200,19 +206,19 @@ export default function Home() {
- {events.length} events loaded + {events.length} {t('app.eventsLoaded', { count: events.length })}
{file && (
- File: {file.name} + {t('app.file')} {file.name}
)} {syncing && (
- Syncing... + {t('app.syncing')}
)}
@@ -228,7 +234,7 @@ export default function Home() { : 'text-gray-600 hover:bg-gray-100' }`} > - Log View + {t('app.logView')}
@@ -267,7 +273,7 @@ export default function Home() { onClick={() => setShowUploadPanel(!showUploadPanel)} className="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-md transition-colors border border-gray-300" > - {showUploadPanel ? 'Hide' : 'Upload'} Log + {showUploadPanel ? t('app.hideLog') : t('app.uploadLog')}
@@ -305,23 +311,23 @@ export default function Home() { {syncing ? (
-

Loading events from server...

+

{t('app.loadingEvents')}

) : ( <> -

No events loaded

+

{t('app.noEventsLoaded')}

diff --git a/frontend/src/components/ProcessTreeView.tsx b/frontend/src/components/ProcessTreeView.tsx index 398092e..d2287dc 100644 --- a/frontend/src/components/ProcessTreeView.tsx +++ b/frontend/src/components/ProcessTreeView.tsx @@ -8,18 +8,20 @@ import { Event } from '@/types/event'; import { buildProcessTree, ProcessNode as ProcessNodeType } from '@/utils/eventParsers'; import { ProcessNode } from './process-tree/ProcessNode'; import { ProcessTreeFiltersComponent, ProcessTreeFilters } from './process-tree/ProcessTreeFilters'; -import { - extractFilterOptions, - filterProcessTree, - getTotalEventCount, - createDefaultFilters +import { + extractFilterOptions, + filterProcessTree, + getTotalEventCount, + createDefaultFilters } from '@/utils/filterUtils'; +import { useTranslation } from '@/i18n'; interface ProcessTreeViewProps { events: Event[]; } export function ProcessTreeView({ events }: ProcessTreeViewProps) { + const { t } = useTranslation(); const [expandedProcesses, setExpandedProcesses] = useState>(new Set()); const [expandedEvents, setExpandedEvents] = useState>(new Set()); const [filters, setFilters] = useState(createDefaultFilters()); @@ -67,9 +69,9 @@ export function ProcessTreeView({ events }: ProcessTreeViewProps) { return (
-

Process Tree & AI Prompts

+

{t('processTree.title')}

- Hierarchical view of processes with their AI prompts and API calls + {t('processTree.subtitle')}

@@ -86,9 +88,9 @@ export function ProcessTreeView({ events }: ProcessTreeViewProps) { {filteredProcessTree.length === 0 ? (
{totalEvents === 0 ? ( - 'No processes to display' + t('processTree.noProcesses') ) : ( - 'No processes match the current filters' + t('processTree.noMatch') )}
) : ( diff --git a/frontend/src/components/ResourceMetricsView.tsx b/frontend/src/components/ResourceMetricsView.tsx index e3be214..88477ff 100644 --- a/frontend/src/components/ResourceMetricsView.tsx +++ b/frontend/src/components/ResourceMetricsView.tsx @@ -5,6 +5,7 @@ import { useState, useMemo } from 'react'; import { Event } from '@/types/event'; +import { useTranslation } from '@/i18n'; interface ResourceMetrics { timestamp: number; @@ -28,6 +29,7 @@ interface ResourceMetricsViewProps { export function ResourceMetricsView({ events }: ResourceMetricsViewProps) { const [selectedProcess, setSelectedProcess] = useState('all'); const [metricType, setMetricType] = useState<'cpu' | 'memory'>('cpu'); + const { t } = useTranslation(); // Extract system events and convert to metrics const metrics = useMemo(() => { @@ -118,9 +120,9 @@ export function ResourceMetricsView({ events }: ResourceMetricsViewProps) { if (metrics.length === 0) { return (
-

No system resource metrics available

+

{t('metrics.noData')}

- System metrics are captured when using --system flag or the record command + {t('metrics.noDataHint')}

); @@ -132,7 +134,7 @@ export function ResourceMetricsView({ events }: ResourceMetricsViewProps) {

- Resource Metrics + {t('metrics.title')}

@@ -146,7 +148,7 @@ export function ResourceMetricsView({ events }: ResourceMetricsViewProps) { : 'text-gray-600 hover:bg-gray-100' }`} > - CPU + {t('metrics.cpu')}
@@ -166,10 +168,10 @@ export function ResourceMetricsView({ events }: ResourceMetricsViewProps) { onChange={(e) => setSelectedProcess(e.target.value)} className="px-3 py-1 text-sm border border-gray-200 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" > - + {processes.map(p => ( ))} @@ -182,31 +184,31 @@ export function ResourceMetricsView({ events }: ResourceMetricsViewProps) {
{stats.avgCpu}%
-
Avg CPU
+
{t('metrics.avgCpu')}
{stats.maxCpu}%
-
Peak CPU
+
{t('metrics.peakCpu')}
{stats.avgMemory} MB
-
Avg Memory
+
{t('metrics.avgMemory')}
{stats.maxMemory} MB
-
Peak Memory
+
{t('metrics.peakMemory')}
{stats.alertCount}
-
Alerts
+
{t('metrics.alerts')}
@@ -217,10 +219,10 @@ export function ResourceMetricsView({ events }: ResourceMetricsViewProps) { {/* Chart Header */}

- {metricType === 'cpu' ? 'CPU Usage Over Time' : 'Memory Usage Over Time'} + {metricType === 'cpu' ? t('metrics.cpuOverTime') : t('metrics.memoryOverTime')}

- {filteredMetrics.length} data points + {t('metrics.dataPoints', { count: filteredMetrics.length })}
@@ -314,7 +316,7 @@ export function ResourceMetricsView({ events }: ResourceMetricsViewProps) { {/* Detailed Table */}
-

Detailed Metrics

+

{t('metrics.detailedMetrics')}

@@ -322,25 +324,25 @@ export function ResourceMetricsView({ events }: ResourceMetricsViewProps) { - Time + {t('metrics.table.time')} - Process + {t('metrics.table.process')} - PID + {t('metrics.table.pid')} - CPU % + {t('metrics.table.cpuPercent')} - Memory (RSS) + {t('metrics.table.memoryRss')} - Threads + {t('metrics.table.threads')} - Children + {t('metrics.table.children')} diff --git a/frontend/src/components/UploadPanel.tsx b/frontend/src/components/UploadPanel.tsx index 2b82027..e71ef05 100644 --- a/frontend/src/components/UploadPanel.tsx +++ b/frontend/src/components/UploadPanel.tsx @@ -4,6 +4,7 @@ 'use client'; import React from 'react'; +import { useTranslation } from '@/i18n'; interface UploadPanelProps { logContent: string; @@ -22,18 +23,19 @@ export function UploadPanel({ onTextPaste, onParseLog }: UploadPanelProps) { + const { t } = useTranslation(); const sampleLogPath = 'collector/ssl.log'; return (

- Upload Log File + {t('upload.title')}

- or + {t('upload.or')}