├── assets └── image.png ├── .gitignore ├── src └── Spector │ ├── ui-src │ ├── tsconfig.json │ ├── src │ │ ├── components │ │ │ ├── TraceList │ │ │ │ ├── TraceList.module.css │ │ │ │ ├── EmptyState.tsx │ │ │ │ ├── EmptyState.module.css │ │ │ │ ├── TraceList.tsx │ │ │ │ ├── TraceGroup.module.css │ │ │ │ ├── TraceGroup.tsx │ │ │ │ ├── ActivityItem.module.css │ │ │ │ └── ActivityItem.tsx │ │ │ ├── common │ │ │ │ ├── Badge.module.css │ │ │ │ ├── Badge.tsx │ │ │ │ ├── Button.tsx │ │ │ │ └── Button.module.css │ │ │ ├── Header │ │ │ │ ├── Header.tsx │ │ │ │ └── Header.module.css │ │ │ ├── Sidebar │ │ │ │ ├── Sidebar.module.css │ │ │ │ └── Sidebar.tsx │ │ │ └── DetailsPanel │ │ │ │ ├── DetailsPanel.module.css │ │ │ │ └── DetailsPanel.tsx │ │ ├── main.tsx │ │ ├── styles │ │ │ ├── variables.css │ │ │ └── global.css │ │ ├── App.css │ │ ├── utils │ │ │ ├── duration.ts │ │ │ ├── hierarchy.ts │ │ │ └── formatting.ts │ │ ├── types │ │ │ └── index.ts │ │ ├── App.tsx │ │ ├── index.css │ │ ├── hooks │ │ │ └── useSSE.ts │ │ ├── assets │ │ │ └── react.svg │ │ └── context │ │ │ └── SpectorContext.tsx │ ├── .gitignore │ ├── index.html │ ├── eslint.config.js │ ├── vite.config.ts │ ├── tsconfig.node.json │ ├── package.json │ ├── tsconfig.app.json │ ├── public │ │ └── vite.svg │ └── README.md │ ├── wwwroot │ └── index.html │ ├── Storage │ └── InMemoryTraceStore.cs │ ├── Config │ └── SpectorOptions.cs │ ├── Models │ └── TraceModels.cs │ ├── Handlers │ ├── SpectorHttpHandlerFilter.cs │ └── SpectorHttpHandler.cs │ ├── Spector.csproj │ ├── Service │ └── ActivityCollectorService.cs │ ├── Middleware │ └── HttpActivityMiddleware.cs │ ├── README.md │ └── SpectorExtensions.cs ├── tests └── NetworkInspector.TestApi │ ├── appsettings.json │ ├── appsettings.Development.json │ ├── NetworkInspector.TestApi.http │ ├── NetworkInspector.TestApi.csproj │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ └── Controllers │ └── TestController.cs ├── LICENSE ├── NetworkInspector.sln.DotSettings.user ├── NetworkInspector.sln └── README.md /assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashwanthkkn/spector/HEAD/assets/image.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | .idea 4 | nupkg 5 | 6 | # Node.js 7 | **/node_modules/ 8 | **/package-lock.json 9 | 10 | # Build output 11 | **/wwwroot/ 12 | -------------------------------------------------------------------------------- /src/Spector/ui-src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tests/NetworkInspector.TestApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /src/Spector/ui-src/src/components/TraceList/TraceList.module.css: -------------------------------------------------------------------------------- 1 | .contentArea { 2 | flex: 1; 3 | overflow-y: auto; 4 | padding: 1.5rem; 5 | } 6 | 7 | .traceList { 8 | display: flex; 9 | flex-direction: column; 10 | gap: 1rem; 11 | } -------------------------------------------------------------------------------- /src/Spector/ui-src/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App.tsx' 4 | 5 | createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /tests/NetworkInspector.TestApi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning", 6 | "NetworkInspector": "Debug", 7 | "System.Net.Http": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Spector/ui-src/.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 | -------------------------------------------------------------------------------- /tests/NetworkInspector.TestApi/NetworkInspector.TestApi.http: -------------------------------------------------------------------------------- 1 | @NetworkInspector.TestApi_HostAddress = 2 | 3 | GET http://localhost:5031/test/http-call/ 4 | Accept: application/json 5 | 6 | ### 7 | 8 | GET http://localhost:5031/test/http-call1/ 9 | Accept: application/json 10 | 11 | ### 12 | 13 | POST http://localhost:5031/test/echo/ 14 | Accept: application/json 15 | 16 | { 17 | "name":"yash" 18 | } -------------------------------------------------------------------------------- /src/Spector/ui-src/src/components/common/Badge.module.css: -------------------------------------------------------------------------------- 1 | .badge { 2 | display: inline-block; 3 | padding: 0.1875rem 0.625rem; 4 | border-radius: 10px; 5 | font-size: 0.6875rem; 6 | font-weight: 600; 7 | } 8 | 9 | .httpin { 10 | background: rgba(74, 158, 255, 0.15); 11 | color: var(--http-in); 12 | } 13 | 14 | .httpout { 15 | background: rgba(52, 211, 153, 0.15); 16 | color: var(--http-out); 17 | } -------------------------------------------------------------------------------- /src/Spector/ui-src/src/components/TraceList/EmptyState.tsx: -------------------------------------------------------------------------------- 1 | 2 | import styles from './EmptyState.module.css'; 3 | 4 | export function EmptyState() { 5 | return ( 6 |
7 |
📡
8 |

Waiting for network activity...

9 | Make API requests to see them appear here 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/Spector/ui-src/src/components/TraceList/EmptyState.module.css: -------------------------------------------------------------------------------- 1 | .emptyState { 2 | text-align: center; 3 | padding: 4rem 2rem; 4 | color: var(--text-secondary); 5 | } 6 | 7 | .emptyIcon { 8 | font-size: 4rem; 9 | margin-bottom: 1rem; 10 | opacity: 0.5; 11 | } 12 | 13 | .emptyState p { 14 | font-size: 1.125rem; 15 | margin-bottom: 0.5rem; 16 | } 17 | 18 | .emptyState small { 19 | color: var(--text-muted); 20 | } -------------------------------------------------------------------------------- /src/Spector/ui-src/src/components/common/Badge.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Badge.module.css'; 3 | 4 | interface BadgeProps { 5 | type: 'httpin' | 'httpout'; 6 | children: React.ReactNode; 7 | } 8 | 9 | export function Badge({ type, children }: BadgeProps) { 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/Spector/ui-src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Spector - Network Inspector 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Spector/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Spector - Network Inspector 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Spector/Storage/InMemoryTraceStore.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using Spector.Models; 3 | 4 | namespace Spector.Storage; 5 | 6 | public class InMemoryTraceStore 7 | { 8 | private readonly ConcurrentQueue _q = new(); 9 | private readonly int _max; 10 | 11 | public InMemoryTraceStore(int max = 5000) => _max = max; 12 | 13 | public void Add(TraceDto dto) 14 | { 15 | _q.Enqueue(dto); 16 | while (_q.Count > _max && _q.TryDequeue(out _)) { } 17 | } 18 | 19 | public List GetAll() => _q.ToList(); 20 | } -------------------------------------------------------------------------------- /src/Spector/Config/SpectorOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Spector.Config; 2 | 3 | public sealed class SpectorOptions 4 | { 5 | public string ActivitySourceName { get; set; } = "Spector.ActivitySource"; 6 | public int InMemoryMaxTraces { get; set; } = 5000; 7 | public int CollectorChannelCapacity { get; set; } = 5000; 8 | public string UiPath { get; set; } = "/spector"; 9 | public string SseEndpoint { get; set; } = "/spector/events"; 10 | public bool RecordRequestBodies { get; set; } = true; // dev-only 11 | public bool RecordResponseBodies { get; set; } = true; 12 | } -------------------------------------------------------------------------------- /tests/NetworkInspector.TestApi/NetworkInspector.TestApi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Spector/ui-src/src/components/common/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Button.module.css'; 3 | 4 | interface ButtonProps extends React.ButtonHTMLAttributes { 5 | variant?: 'primary' | 'secondary'; 6 | children: React.ReactNode; 7 | } 8 | 9 | export function Button({ variant = 'secondary', children, className, ...props }: ButtonProps) { 10 | return ( 11 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/Spector/ui-src/src/styles/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --bg-primary: #0e1117; 3 | --bg-secondary: #1a1d29; 4 | --bg-tertiary: #252837; 5 | --bg-hover: #2d3142; 6 | --border-color: #363b4e; 7 | --text-primary: #e8eaed; 8 | --text-secondary: #9aa0a6; 9 | --text-muted: #5f6368; 10 | --accent-blue: #4a9eff; 11 | --accent-green: #34d399; 12 | --accent-orange: #fb923c; 13 | --accent-red: #f87171; 14 | --accent-purple: #a78bfa; 15 | --http-in: #4a9eff; 16 | --http-out: #34d399; 17 | --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2); 18 | --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3); 19 | } 20 | -------------------------------------------------------------------------------- /tests/NetworkInspector.TestApi/Program.cs: -------------------------------------------------------------------------------- 1 | using NetworkInspector; 2 | using Spector; 3 | 4 | var builder = WebApplication.CreateBuilder(args); 5 | 6 | // Add services to the container. 7 | builder.Services.AddLogging(); 8 | builder.Services.AddControllers(); 9 | builder.Services.AddEndpointsApiExplorer(); 10 | builder.Services.AddHttpClient(); 11 | 12 | // Add Network Inspector - this is the ONLY line needed! 13 | builder.Services.AddSpector(); 14 | 15 | var app = builder.Build(); 16 | 17 | // Configure the HTTP request pipeline. 18 | app.UseSpector(); 19 | app.UseAuthorization(); 20 | 21 | // Use Network Inspector Middleware 22 | //app.UseNetworkInspector(); 23 | 24 | 25 | app.MapControllers(); 26 | 27 | app.Run(); 28 | -------------------------------------------------------------------------------- /src/Spector/ui-src/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | import { defineConfig, globalIgnores } from 'eslint/config' 7 | 8 | export default defineConfig([ 9 | globalIgnores(['dist']), 10 | { 11 | files: ['**/*.{ts,tsx}'], 12 | extends: [ 13 | js.configs.recommended, 14 | tseslint.configs.recommended, 15 | reactHooks.configs.flat.recommended, 16 | reactRefresh.configs.vite, 17 | ], 18 | languageOptions: { 19 | ecmaVersion: 2020, 20 | globals: globals.browser, 21 | }, 22 | }, 23 | ]) 24 | -------------------------------------------------------------------------------- /src/Spector/ui-src/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | base: './', // Use relative paths for assets 8 | build: { 9 | outDir: '../wwwroot', 10 | emptyOutDir: true, 11 | rollupOptions: { 12 | output: { 13 | entryFileNames: 'assets/[name]-src.js', 14 | chunkFileNames: 'assets/[name]-src.js', 15 | assetFileNames: 'assets/[name]-src.[ext]' 16 | } 17 | } 18 | }, 19 | server: { 20 | port: 5173, 21 | proxy: { 22 | '/spector': { 23 | target: 'http://localhost:5000', 24 | changeOrigin: true 25 | } 26 | } 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /src/Spector/ui-src/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2023", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "types": ["node"], 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "verbatimModuleSyntax": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "erasableSyntaxOnly": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true 24 | }, 25 | "include": ["vite.config.ts"] 26 | } 27 | -------------------------------------------------------------------------------- /src/Spector/ui-src/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /src/Spector/ui-src/src/components/common/Button.module.css: -------------------------------------------------------------------------------- 1 | .btn { 2 | padding: 0.5rem 1.25rem; 3 | border: none; 4 | border-radius: 6px; 5 | font-size: 0.875rem; 6 | font-weight: 500; 7 | cursor: pointer; 8 | transition: all 0.2s; 9 | outline: none; 10 | } 11 | 12 | .secondary { 13 | background: var(--bg-tertiary); 14 | color: var(--text-primary); 15 | border: 1px solid var(--border-color); 16 | } 17 | 18 | .secondary:hover { 19 | background: var(--bg-hover); 20 | transform: translateY(-1px); 21 | } 22 | 23 | .secondary:active { 24 | transform: translateY(0); 25 | } 26 | 27 | .primary { 28 | background: var(--accent-blue); 29 | color: white; 30 | } 31 | 32 | .primary:hover { 33 | background: var(--accent-purple); 34 | transform: translateY(-1px); 35 | } -------------------------------------------------------------------------------- /src/Spector/ui-src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui-src", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^19.2.0", 14 | "react-dom": "^19.2.0" 15 | }, 16 | "devDependencies": { 17 | "@eslint/js": "^9.39.1", 18 | "@types/node": "^24.10.1", 19 | "@types/react": "^19.2.5", 20 | "@types/react-dom": "^19.2.3", 21 | "@vitejs/plugin-react": "^5.1.1", 22 | "eslint": "^9.39.1", 23 | "eslint-plugin-react-hooks": "^7.0.1", 24 | "eslint-plugin-react-refresh": "^0.4.24", 25 | "globals": "^16.5.0", 26 | "typescript": "~5.9.3", 27 | "typescript-eslint": "^8.46.4", 28 | "vite": "^7.2.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Spector/Models/TraceModels.cs: -------------------------------------------------------------------------------- 1 | namespace Spector.Models; 2 | 3 | public record TraceEventDto 4 | { 5 | public string Name { get; init; } = string.Empty; 6 | public DateTimeOffset Timestamp { get; init; } 7 | public Dictionary Tags { get; init; } = new(); 8 | } 9 | 10 | public record TraceDto 11 | { 12 | public string Name { get; init; } = string.Empty; 13 | public string TraceId { get; init; } = string.Empty; 14 | public string SpanId { get; init; } = string.Empty; 15 | public string ParentSpanId { get; init; } = string.Empty; 16 | public DateTime StartTimeUtc { get; init; } 17 | public TimeSpan Duration { get; init; } 18 | public string Kind { get; init; } = string.Empty; 19 | public Dictionary Tags { get; init; } = new(); 20 | public List Events { get; init; } = new(); 21 | } -------------------------------------------------------------------------------- /src/Spector/ui-src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2022", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "types": ["vite/client"], 9 | "skipLibCheck": true, 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "moduleDetection": "force", 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "erasableSyntaxOnly": true, 24 | "noFallthroughCasesInSwitch": true, 25 | "noUncheckedSideEffectImports": true 26 | }, 27 | "include": ["src"] 28 | } 29 | -------------------------------------------------------------------------------- /src/Spector/ui-src/src/utils/duration.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Parse duration string from format "HH:MM:SS.mmmmmmm" to milliseconds 3 | */ 4 | export function parseDuration(duration: string): number { 5 | const parts = duration.split(':'); 6 | const hours = parseInt(parts[0]); 7 | const minutes = parseInt(parts[1]); 8 | const secondsParts = parts[2].split('.'); 9 | const seconds = parseInt(secondsParts[0]); 10 | const milliseconds = secondsParts[1] ? parseInt(secondsParts[1].substring(0, 3)) : 0; 11 | 12 | return (hours * 3600 + minutes * 60 + seconds) * 1000 + milliseconds; 13 | } 14 | 15 | /** 16 | * Format milliseconds to human-readable duration 17 | */ 18 | export function formatDuration(ms: number): string { 19 | if (ms < 1) return `${(ms * 1000).toFixed(0)}μs`; 20 | if (ms < 1000) return `${ms.toFixed(0)}ms`; 21 | return `${(ms / 1000).toFixed(2)}s`; 22 | } 23 | -------------------------------------------------------------------------------- /src/Spector/ui-src/src/types/index.ts: -------------------------------------------------------------------------------- 1 | // Activity data structure from SSE 2 | export interface Activity { 3 | TraceId: string; 4 | SpanId: string; 5 | ParentSpanId: string | null; 6 | Name: string; 7 | StartTimeUtc: string; 8 | Duration: string; 9 | Tags: Record; 10 | } 11 | 12 | // Trace group containing multiple activities 13 | export interface Trace { 14 | traceId: string; 15 | activities: Activity[]; 16 | startTime: Date; 17 | endTime: Date; 18 | } 19 | 20 | // Filter state 21 | export interface Filters { 22 | httpIn: boolean; 23 | httpOut: boolean; 24 | } 25 | 26 | // Connection status 27 | export type ConnectionStatus = 'connected' | 'disconnected' | 'connecting'; 28 | 29 | // Activity type 30 | export type ActivityType = 'httpin' | 'httpout'; 31 | 32 | // HTTP methods 33 | export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD'; 34 | 35 | // Status code ranges 36 | export type StatusType = 'success' | 'redirect' | 'client-error' | 'server-error' | 'error'; 37 | -------------------------------------------------------------------------------- /src/Spector/ui-src/src/components/TraceList/TraceList.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { useSpector } from '../../context/SpectorContext'; 3 | import { TraceGroup } from './TraceGroup'; 4 | import { EmptyState } from './EmptyState'; 5 | import styles from './TraceList.module.css'; 6 | 7 | export function TraceList() { 8 | const { traces } = useSpector(); 9 | 10 | const sortedTraces = useMemo(() => { 11 | return Array.from(traces.values()) 12 | .sort((a, b) => b.startTime.getTime() - a.startTime.getTime()); 13 | }, [traces]); 14 | 15 | if (sortedTraces.length === 0) { 16 | return ( 17 |
18 | 19 |
20 | ); 21 | } 22 | 23 | return ( 24 |
25 |
26 | {sortedTraces.map(trace => ( 27 | 28 | ))} 29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Yashwanth K 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Spector/ui-src/src/utils/hierarchy.ts: -------------------------------------------------------------------------------- 1 | import type { Activity } from '../types'; 2 | 3 | /** 4 | * Build parent-child hierarchy from flat activity list 5 | */ 6 | export function buildActivityHierarchy(activities: Activity[]): { 7 | rootActivities: Activity[]; 8 | childrenMap: Map; 9 | } { 10 | const rootActivities: Activity[] = []; 11 | const childrenMap = new Map(); 12 | const activityMap = new Map(); 13 | 14 | // Create activity lookup map 15 | activities.forEach(activity => { 16 | activityMap.set(activity.SpanId, activity); 17 | }); 18 | 19 | // Build hierarchy 20 | activities.forEach(activity => { 21 | if (!activity.ParentSpanId || !activityMap.has(activity.ParentSpanId)) { 22 | rootActivities.push(activity); 23 | } else { 24 | if (!childrenMap.has(activity.ParentSpanId)) { 25 | childrenMap.set(activity.ParentSpanId, []); 26 | } 27 | childrenMap.get(activity.ParentSpanId)!.push(activity); 28 | } 29 | }); 30 | 31 | return { rootActivities, childrenMap }; 32 | } 33 | -------------------------------------------------------------------------------- /src/Spector/ui-src/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { SpectorProvider, useSpector } from './context/SpectorContext'; 2 | import { useSSE } from './hooks/useSSE'; 3 | import { Header } from './components/Header/Header'; 4 | import { Sidebar } from './components/Sidebar/Sidebar'; 5 | import { TraceList } from './components/TraceList/TraceList'; 6 | import { DetailsPanel } from './components/DetailsPanel/DetailsPanel'; 7 | import './styles/variables.css'; 8 | import './styles/global.css'; 9 | 10 | function AppContent() { 11 | const { addActivity, setConnectionStatus, isPaused } = useSpector(); 12 | 13 | useSSE({ 14 | url: '/spector/events', 15 | onMessage: addActivity, 16 | onStatusChange: setConnectionStatus, 17 | isPaused 18 | }); 19 | 20 | return ( 21 |
22 |
23 |
24 | 25 | 26 | 27 |
28 |
29 | ); 30 | } 31 | 32 | function App() { 33 | return ( 34 | 35 | 36 | 37 | ); 38 | } 39 | 40 | export default App; 41 | -------------------------------------------------------------------------------- /NetworkInspector.sln.DotSettings.user: -------------------------------------------------------------------------------- 1 | 2 | ForceIncluded 3 | ForceIncluded -------------------------------------------------------------------------------- /src/Spector/ui-src/src/styles/global.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; 9 | background: var(--bg-primary); 10 | color: var(--text-primary); 11 | line-height: 1.6; 12 | overflow: hidden; 13 | } 14 | 15 | .container { 16 | height: 100vh; 17 | display: flex; 18 | flex-direction: column; 19 | } 20 | 21 | /* Scrollbar Styling */ 22 | ::-webkit-scrollbar { 23 | width: 8px; 24 | height: 8px; 25 | } 26 | 27 | ::-webkit-scrollbar-track { 28 | background: var(--bg-primary); 29 | } 30 | 31 | ::-webkit-scrollbar-thumb { 32 | background: var(--border-color); 33 | border-radius: 4px; 34 | } 35 | 36 | ::-webkit-scrollbar-thumb:hover { 37 | background: var(--text-muted); 38 | } 39 | 40 | /* Animations */ 41 | @keyframes pulse { 42 | 43 | 0%, 44 | 100% { 45 | opacity: 1; 46 | } 47 | 48 | 50% { 49 | opacity: 0.5; 50 | } 51 | } 52 | 53 | @keyframes slideIn { 54 | from { 55 | opacity: 0; 56 | transform: translateY(-10px); 57 | } 58 | 59 | to { 60 | opacity: 1; 61 | transform: translateY(0); 62 | } 63 | } -------------------------------------------------------------------------------- /tests/NetworkInspector.TestApi/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:50588", 8 | "sslPort": 44356 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": false, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5031", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7226;http://localhost:5031", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Spector/Handlers/SpectorHttpHandlerFilter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Http; 3 | 4 | namespace Spector.Handlers; 5 | 6 | public class SpectorHttpHandlerFilter : IHttpMessageHandlerBuilderFilter 7 | { 8 | private readonly IServiceProvider _serviceProvider; 9 | 10 | public SpectorHttpHandlerFilter(IServiceProvider serviceProvider) 11 | { 12 | _serviceProvider = serviceProvider; 13 | } 14 | 15 | public Action Configure(Action next) 16 | { 17 | return builder => 18 | { 19 | // run the rest of the builders first 20 | next(builder); 21 | 22 | // add our handler instance from DI (transient) 23 | var handler = _serviceProvider.GetRequiredService(); 24 | if (handler != null) 25 | { 26 | // wrapping: if there is already a primary handler assign its InnerHandler accordingly 27 | // We put tracingHandler at the end of the pipeline so it is the outermost delegating handler. 28 | handler.InnerHandler = builder.PrimaryHandler ?? new HttpClientHandler(); 29 | builder.PrimaryHandler = handler; 30 | } 31 | }; 32 | } 33 | } -------------------------------------------------------------------------------- /src/Spector/ui-src/src/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { useSpector } from '../../context/SpectorContext'; 3 | import { Button } from '../common/Button'; 4 | import styles from './Header.module.css'; 5 | 6 | export function Header() { 7 | const { clearAll, togglePause, isPaused, connectionStatus } = useSpector(); 8 | 9 | return ( 10 |
11 |
12 |

🔍 Spector

13 |
14 | 15 | {connectionStatus === 'connected' ? 'Connected' : connectionStatus === 'disconnected' ? 'Disconnected' : 'Connecting...'} 16 |
17 |
18 |
19 | 20 | 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/Spector/ui-src/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | a:hover { 22 | color: #535bf2; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | display: flex; 28 | place-items: center; 29 | min-width: 320px; 30 | min-height: 100vh; 31 | } 32 | 33 | h1 { 34 | font-size: 3.2em; 35 | line-height: 1.1; 36 | } 37 | 38 | button { 39 | border-radius: 8px; 40 | border: 1px solid transparent; 41 | padding: 0.6em 1.2em; 42 | font-size: 1em; 43 | font-weight: 500; 44 | font-family: inherit; 45 | background-color: #1a1a1a; 46 | cursor: pointer; 47 | transition: border-color 0.25s; 48 | } 49 | button:hover { 50 | border-color: #646cff; 51 | } 52 | button:focus, 53 | button:focus-visible { 54 | outline: 4px auto -webkit-focus-ring-color; 55 | } 56 | 57 | @media (prefers-color-scheme: light) { 58 | :root { 59 | color: #213547; 60 | background-color: #ffffff; 61 | } 62 | a:hover { 63 | color: #747bff; 64 | } 65 | button { 66 | background-color: #f9f9f9; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Spector/ui-src/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Spector/ui-src/src/components/Header/Header.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | background: var(--bg-secondary); 3 | border-bottom: 1px solid var(--border-color); 4 | padding: 0.75rem 1.25rem; 5 | display: flex; 6 | justify-content: space-between; 7 | align-items: center; 8 | box-shadow: var(--shadow); 9 | z-index: 10; 10 | } 11 | 12 | .headerContent { 13 | display: flex; 14 | align-items: center; 15 | gap: 1.5rem; 16 | } 17 | 18 | .title { 19 | font-size: 1.25rem; 20 | font-weight: 600; 21 | background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); 22 | -webkit-background-clip: text; 23 | -webkit-text-fill-color: transparent; 24 | background-clip: text; 25 | } 26 | 27 | .connectionStatus { 28 | display: flex; 29 | align-items: center; 30 | gap: 0.5rem; 31 | padding: 0.375rem 0.875rem; 32 | background: var(--bg-tertiary); 33 | border-radius: 16px; 34 | font-size: 0.8125rem; 35 | } 36 | 37 | .statusIndicator { 38 | width: 7px; 39 | height: 7px; 40 | border-radius: 50%; 41 | background: var(--text-muted); 42 | } 43 | 44 | .statusIndicator.connected { 45 | background: var(--accent-green); 46 | animation: pulse 2s infinite; 47 | } 48 | 49 | .statusIndicator.disconnected { 50 | background: var(--accent-red); 51 | } 52 | 53 | .statusIndicator.connecting { 54 | background: var(--accent-orange); 55 | animation: pulse 2s infinite; 56 | } 57 | 58 | .controls { 59 | display: flex; 60 | gap: 0.75rem; 61 | } 62 | 63 | @media (max-width: 768px) { 64 | .header { 65 | flex-direction: column; 66 | gap: 1rem; 67 | align-items: flex-start; 68 | } 69 | } -------------------------------------------------------------------------------- /src/Spector/ui-src/src/components/TraceList/TraceGroup.module.css: -------------------------------------------------------------------------------- 1 | .traceGroup { 2 | background: var(--bg-secondary); 3 | border: 1px solid var(--border-color); 4 | border-radius: 6px; 5 | overflow: hidden; 6 | transition: all 0.3s; 7 | animation: slideIn 0.3s ease-out; 8 | } 9 | 10 | .traceHeader { 11 | padding: 0.625rem 1rem; 12 | background: var(--bg-tertiary); 13 | border-bottom: 1px solid var(--border-color); 14 | cursor: pointer; 15 | display: flex; 16 | justify-content: space-between; 17 | align-items: center; 18 | transition: background 0.2s; 19 | } 20 | 21 | .traceHeader:hover { 22 | background: var(--bg-hover); 23 | } 24 | 25 | .traceHeader.collapsed { 26 | border-bottom: none; 27 | } 28 | 29 | .traceInfo { 30 | flex: 1; 31 | } 32 | 33 | .traceTitle { 34 | font-size: 0.75rem; 35 | color: var(--text-secondary); 36 | margin-bottom: 0.125rem; 37 | } 38 | 39 | .traceId { 40 | font-family: 'Courier New', monospace; 41 | font-size: 0.6875rem; 42 | color: var(--text-muted); 43 | } 44 | 45 | .traceMeta { 46 | display: flex; 47 | align-items: center; 48 | gap: 0.75rem; 49 | } 50 | 51 | .traceDuration { 52 | font-size: 0.75rem; 53 | font-weight: 600; 54 | color: var(--accent-orange); 55 | } 56 | 57 | .traceCount { 58 | font-size: 0.6875rem; 59 | padding: 0.125rem 0.5rem; 60 | background: var(--bg-primary); 61 | border-radius: 10px; 62 | color: var(--text-secondary); 63 | } 64 | 65 | .collapseIcon { 66 | font-size: 1rem; 67 | color: var(--text-secondary); 68 | transition: transform 0.3s; 69 | } 70 | 71 | .collapseIcon.collapsed { 72 | transform: rotate(-90deg); 73 | } 74 | 75 | .traceActivities { 76 | padding: 0.375rem; 77 | } -------------------------------------------------------------------------------- /src/Spector/ui-src/src/components/Sidebar/Sidebar.module.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | width: 250px; 3 | background: var(--bg-secondary); 4 | border-right: 1px solid var(--border-color); 5 | padding: 1.5rem; 6 | overflow-y: auto; 7 | } 8 | 9 | .sidebar h3 { 10 | font-size: 0.875rem; 11 | font-weight: 600; 12 | text-transform: uppercase; 13 | letter-spacing: 0.5px; 14 | color: var(--text-secondary); 15 | margin-bottom: 1rem; 16 | } 17 | 18 | .filterSection { 19 | margin-bottom: 2rem; 20 | } 21 | 22 | .filterGroup { 23 | display: flex; 24 | flex-direction: column; 25 | gap: 0.75rem; 26 | } 27 | 28 | .filterGroup label { 29 | display: flex; 30 | align-items: center; 31 | cursor: pointer; 32 | padding: 0.5rem; 33 | border-radius: 6px; 34 | transition: background 0.2s; 35 | } 36 | 37 | .filterGroup label:hover { 38 | background: var(--bg-tertiary); 39 | } 40 | 41 | .filterGroup input[type="checkbox"] { 42 | margin-right: 0.75rem; 43 | width: 16px; 44 | height: 16px; 45 | cursor: pointer; 46 | } 47 | 48 | .filterLabel { 49 | font-size: 0.875rem; 50 | padding: 0.25rem 0.75rem; 51 | border-radius: 4px; 52 | font-weight: 500; 53 | } 54 | 55 | .filterLabel.httpIn { 56 | background: rgba(74, 158, 255, 0.15); 57 | color: var(--http-in); 58 | } 59 | 60 | .filterLabel.httpOut { 61 | background: rgba(52, 211, 153, 0.15); 62 | color: var(--http-out); 63 | } 64 | 65 | .statsSection { 66 | padding-top: 1.5rem; 67 | border-top: 1px solid var(--border-color); 68 | } 69 | 70 | .statItem { 71 | display: flex; 72 | justify-content: space-between; 73 | padding: 0.5rem 0; 74 | font-size: 0.875rem; 75 | } 76 | 77 | .statLabel { 78 | color: var(--text-secondary); 79 | } 80 | 81 | .statValue { 82 | font-weight: 600; 83 | color: var(--accent-blue); 84 | } 85 | 86 | @media (max-width: 768px) { 87 | .sidebar { 88 | display: none; 89 | } 90 | } -------------------------------------------------------------------------------- /src/Spector/ui-src/src/components/Sidebar/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { useSpector } from '../../context/SpectorContext'; 3 | import styles from './Sidebar.module.css'; 4 | 5 | export function Sidebar() { 6 | const { filters, setFilter, traces, activities } = useSpector(); 7 | 8 | return ( 9 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /NetworkInspector.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4CB7EB44-1422-42E0-BA49-0402DE7ED1C9}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{C76BB15B-F3FF-430B-AD6C-7BD27CEA938A}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetworkInspector.TestApi", "tests\NetworkInspector.TestApi\NetworkInspector.TestApi.csproj", "{AFD1C0F1-0B77-4B0E-85E0-4E1754D7022B}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spector", "src\Spector\Spector.csproj", "{F23511D3-1558-43FE-9A05-98172BB62205}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {AFD1C0F1-0B77-4B0E-85E0-4E1754D7022B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {AFD1C0F1-0B77-4B0E-85E0-4E1754D7022B}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {AFD1C0F1-0B77-4B0E-85E0-4E1754D7022B}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {AFD1C0F1-0B77-4B0E-85E0-4E1754D7022B}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {F23511D3-1558-43FE-9A05-98172BB62205}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {F23511D3-1558-43FE-9A05-98172BB62205}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {F23511D3-1558-43FE-9A05-98172BB62205}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {F23511D3-1558-43FE-9A05-98172BB62205}.Release|Any CPU.Build.0 = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(NestedProjects) = preSolution 33 | {AFD1C0F1-0B77-4B0E-85E0-4E1754D7022B} = {C76BB15B-F3FF-430B-AD6C-7BD27CEA938A} 34 | {F23511D3-1558-43FE-9A05-98172BB62205} = {4CB7EB44-1422-42E0-BA49-0402DE7ED1C9} 35 | EndGlobalSection 36 | EndGlobal 37 | -------------------------------------------------------------------------------- /src/Spector/ui-src/src/utils/formatting.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Format JSON string with proper indentation 3 | * Handles nested JSON strings and wrapper objects 4 | */ 5 | export function formatJson(jsonString: string): string { 6 | if (!jsonString || jsonString.trim() === '') { 7 | return ''; 8 | } 9 | 10 | try { 11 | // First, try to parse the string as JSON 12 | let parsed = JSON.parse(jsonString); 13 | 14 | // Check if it's a wrapper object with 'content' field 15 | if (parsed && typeof parsed === 'object' && parsed.content !== undefined) { 16 | // If content is a string, try to parse it as JSON 17 | if (typeof parsed.content === 'string') { 18 | try { 19 | const innerParsed = JSON.parse(parsed.content); 20 | // Use the inner parsed content instead 21 | parsed = innerParsed; 22 | } catch { 23 | // If parsing fails, use the content string as-is 24 | return parsed.content; 25 | } 26 | } else { 27 | // If content is already an object, use it 28 | parsed = parsed.content; 29 | } 30 | } 31 | 32 | // Recursively unwrap if there are more nested JSON strings 33 | if (typeof parsed === 'string') { 34 | try { 35 | const innerParsed = JSON.parse(parsed); 36 | parsed = innerParsed; 37 | } catch { 38 | // Not JSON, return as-is 39 | return parsed; 40 | } 41 | } 42 | 43 | // Format the final parsed object 44 | return JSON.stringify(parsed, null, 2); 45 | } catch (error) { 46 | // If parsing fails, return the original string 47 | return jsonString; 48 | } 49 | } 50 | 51 | /** 52 | * Escape HTML to prevent XSS 53 | */ 54 | export function escapeHtml(text: string): string { 55 | const div = document.createElement('div'); 56 | div.textContent = text; 57 | return div.innerHTML; 58 | } 59 | -------------------------------------------------------------------------------- /src/Spector/Spector.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | Spector 10 | 1.0.1 11 | Yashwanth K 12 | Yashwanth K 13 | A lightweight network and dependency inspector for ASP.NET Core applications. Spector captures HTTP traces (incoming and outgoing requests) and provides a real-time web UI for monitoring and debugging. 14 | aspnetcore;network;inspector;tracing;diagnostics;http;monitoring;debugging 15 | https://github.com/yashwanthkkn/spector 16 | https://github.com/yashwanthkkn/spector 17 | git 18 | MIT 19 | README.md 20 | 21 | 22 | false 23 | true 24 | snupkg 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/Spector/ui-src/src/components/TraceList/TraceGroup.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import type { Trace } from '../../types'; 3 | import { ActivityItem } from './ActivityItem'; 4 | import { buildActivityHierarchy } from '../../utils/hierarchy'; 5 | import { formatDuration } from '../../utils/duration'; 6 | import styles from './TraceGroup.module.css'; 7 | 8 | interface TraceGroupProps { 9 | trace: Trace; 10 | } 11 | 12 | export function TraceGroup({ trace }: TraceGroupProps) { 13 | const [isCollapsed, setIsCollapsed] = useState(false); 14 | 15 | const totalDuration = trace.endTime.getTime() - trace.startTime.getTime(); 16 | const activityCount = trace.activities.length; 17 | 18 | const { rootActivities, childrenMap } = buildActivityHierarchy(trace.activities); 19 | 20 | const toggleCollapse = () => { 21 | setIsCollapsed(!isCollapsed); 22 | }; 23 | 24 | return ( 25 |
26 |
30 |
31 |
Trace
32 |
{trace.traceId}
33 |
34 |
35 | {formatDuration(totalDuration)} 36 | 37 | {activityCount} request{activityCount !== 1 ? 's' : ''} 38 | 39 | 40 | ▼ 41 | 42 |
43 |
44 | {!isCollapsed && ( 45 |
46 | {rootActivities.map(activity => ( 47 | 54 | ))} 55 |
56 | )} 57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/Spector/ui-src/src/hooks/useSSE.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import type { Activity, ConnectionStatus } from '../types'; 3 | 4 | interface UseSSEOptions { 5 | url: string; 6 | onMessage: (activity: Activity) => void; 7 | onStatusChange: (status: ConnectionStatus) => void; 8 | isPaused: boolean; 9 | } 10 | 11 | export function useSSE({ url, onMessage, onStatusChange, isPaused }: UseSSEOptions) { 12 | const eventSourceRef = useRef(null); 13 | const reconnectTimeoutRef = useRef(null); 14 | 15 | useEffect(() => { 16 | function connect() { 17 | // Clean up existing connection 18 | if (eventSourceRef.current) { 19 | eventSourceRef.current.close(); 20 | } 21 | 22 | const eventSource = new EventSource(url); 23 | eventSourceRef.current = eventSource; 24 | 25 | eventSource.onopen = () => { 26 | onStatusChange('connected'); 27 | console.log('SSE Connected'); 28 | }; 29 | 30 | eventSource.onmessage = (event) => { 31 | if (isPaused) return; 32 | 33 | try { 34 | const activity: Activity = JSON.parse(event.data); 35 | console.log(activity); 36 | onMessage(activity); 37 | } catch (error) { 38 | console.error('Error parsing SSE data:', error); 39 | } 40 | }; 41 | 42 | eventSource.onerror = () => { 43 | onStatusChange('disconnected'); 44 | console.error('SSE Error'); 45 | 46 | // Attempt to reconnect after 3 seconds 47 | if (reconnectTimeoutRef.current) { 48 | clearTimeout(reconnectTimeoutRef.current); 49 | } 50 | 51 | reconnectTimeoutRef.current = window.setTimeout(() => { 52 | if (eventSource.readyState === EventSource.CLOSED) { 53 | connect(); 54 | } 55 | }, 3000); 56 | }; 57 | } 58 | 59 | connect(); 60 | 61 | // Cleanup on unmount 62 | return () => { 63 | if (eventSourceRef.current) { 64 | eventSourceRef.current.close(); 65 | } 66 | if (reconnectTimeoutRef.current) { 67 | clearTimeout(reconnectTimeoutRef.current); 68 | } 69 | }; 70 | }, [url, onMessage, onStatusChange, isPaused]); 71 | } 72 | -------------------------------------------------------------------------------- /tests/NetworkInspector.TestApi/Controllers/TestController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace NetworkInspector.TestApi.Controllers 4 | { 5 | [ApiController] 6 | [Route("[controller]")] 7 | public class TestController : ControllerBase 8 | { 9 | private readonly IHttpClientFactory _httpClientFactory; 10 | 11 | public TestController(IHttpClientFactory httpClientFactory) 12 | { 13 | _httpClientFactory = httpClientFactory; 14 | } 15 | 16 | [HttpGet("ping")] 17 | public IActionResult Ping() 18 | { 19 | return Ok(new { Message = "Pong", Time = DateTime.UtcNow }); 20 | } 21 | 22 | [HttpPost("echo")] 23 | public IActionResult Echo([FromBody] object data) 24 | { 25 | return Ok(data); 26 | } 27 | 28 | [HttpGet("http-call")] 29 | public async Task MakeHttpCall() 30 | { 31 | // Call a public API using HttpClientFactory for proper Activity tracking 32 | try 33 | { 34 | var httpClient = _httpClientFactory.CreateClient("my-client"); 35 | var response = await httpClient.GetAsync("https://jsonplaceholder.typicode.com/todos/1"); 36 | var content = await response.Content.ReadAsStringAsync(); 37 | return Ok(new { Status = response.StatusCode, Content = content }); 38 | } 39 | catch (Exception ex) 40 | { 41 | return StatusCode(500, new { Error = ex.Message }); 42 | } 43 | } 44 | 45 | [HttpGet("http-call1")] 46 | public async Task MakeHttpCall1() 47 | { 48 | // Call a public API using HttpClientFactory for proper Activity tracking 49 | try 50 | { 51 | var httpClient = _httpClientFactory.CreateClient("my-client"); 52 | var response = await httpClient.GetAsync("https://jsonplaceholder.typicode.com/todos/1"); 53 | var response1 = await httpClient.GetAsync("https://jsonplaceholsdsdder.typicode.com/todos/1"); 54 | var content = await response.Content.ReadAsStringAsync(); 55 | return Ok(new { Status = response.StatusCode, Content = content }); 56 | } 57 | catch (Exception ex) 58 | { 59 | return StatusCode(500, new { Error = ex.Message }); 60 | } 61 | } 62 | 63 | [HttpGet("error")] 64 | public IActionResult Error() 65 | { 66 | return StatusCode(500, new { Error = "Something went wrong" }); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Spector/ui-src/src/components/DetailsPanel/DetailsPanel.module.css: -------------------------------------------------------------------------------- 1 | .detailsPanel { 2 | width: 0; 3 | background: var(--bg-secondary); 4 | border-left: 1px solid var(--border-color); 5 | overflow: hidden; 6 | transition: width 0.3s; 7 | } 8 | 9 | .detailsPanel.open { 10 | width: 450px; 11 | } 12 | 13 | .detailsHeader { 14 | padding: 0.75rem 1rem; 15 | background: var(--bg-tertiary); 16 | border-bottom: 1px solid var(--border-color); 17 | display: flex; 18 | justify-content: space-between; 19 | align-items: center; 20 | } 21 | 22 | .detailsHeader h3 { 23 | font-size: 0.875rem; 24 | font-weight: 600; 25 | } 26 | 27 | .closeBtn { 28 | background: none; 29 | border: none; 30 | color: var(--text-secondary); 31 | font-size: 1.25rem; 32 | cursor: pointer; 33 | padding: 0; 34 | width: 26px; 35 | height: 26px; 36 | display: flex; 37 | align-items: center; 38 | justify-content: center; 39 | border-radius: 4px; 40 | transition: all 0.2s; 41 | } 42 | 43 | .closeBtn:hover { 44 | background: var(--bg-hover); 45 | color: var(--text-primary); 46 | } 47 | 48 | .detailsContent { 49 | padding: 1rem; 50 | overflow-y: auto; 51 | height: calc(100vh - 100px); 52 | } 53 | 54 | .detailsPlaceholder { 55 | color: var(--text-secondary); 56 | text-align: center; 57 | padding: 2rem; 58 | } 59 | 60 | .detailSection { 61 | margin-bottom: 1.25rem; 62 | } 63 | 64 | .detailSection h4 { 65 | font-size: 0.6875rem; 66 | font-weight: 600; 67 | text-transform: uppercase; 68 | letter-spacing: 0.5px; 69 | color: var(--text-secondary); 70 | margin-bottom: 0.625rem; 71 | } 72 | 73 | .detailRow { 74 | display: flex; 75 | padding: 0.375rem 0; 76 | border-bottom: 1px solid var(--border-color); 77 | font-size: 0.8125rem; 78 | } 79 | 80 | .detailRow:last-child { 81 | border-bottom: none; 82 | } 83 | 84 | .detailLabel { 85 | width: 100px; 86 | color: var(--text-secondary); 87 | flex-shrink: 0; 88 | font-size: 0.75rem; 89 | } 90 | 91 | .detailValue { 92 | flex: 1; 93 | color: var(--text-primary); 94 | word-break: break-all; 95 | font-family: 'Courier New', monospace; 96 | font-size: 0.75rem; 97 | } 98 | 99 | .codeBlock { 100 | background: var(--bg-primary); 101 | border: 1px solid var(--border-color); 102 | border-radius: 6px; 103 | padding: 0.75rem; 104 | overflow-x: auto; 105 | font-family: 'Courier New', monospace; 106 | font-size: 0.75rem; 107 | line-height: 1.5; 108 | max-height: 400px; 109 | overflow-y: auto; 110 | } 111 | 112 | .codeBlock pre { 113 | margin: 0; 114 | white-space: pre-wrap; 115 | word-wrap: break-word; 116 | } 117 | 118 | .activityMethod { 119 | font-size: 0.6875rem; 120 | padding: 0.0625rem 0.375rem; 121 | border-radius: 3px; 122 | font-weight: 600; 123 | background: var(--bg-tertiary); 124 | } 125 | 126 | .methodGET { 127 | color: var(--accent-green); 128 | } 129 | 130 | .methodPOST { 131 | color: var(--accent-blue); 132 | } 133 | 134 | .methodPUT { 135 | color: var(--accent-orange); 136 | } 137 | 138 | .methodDELETE { 139 | color: var(--accent-red); 140 | } 141 | 142 | @media (max-width: 768px) { 143 | .detailsPanel.open { 144 | position: absolute; 145 | right: 0; 146 | top: 0; 147 | height: 100%; 148 | width: 100%; 149 | z-index: 20; 150 | } 151 | } -------------------------------------------------------------------------------- /src/Spector/ui-src/README.md: -------------------------------------------------------------------------------- 1 | # Spector UI (React + Vite) 2 | 3 | This directory contains the React-based UI for the Spector network inspector. 4 | 5 | ## Development 6 | 7 | ### Prerequisites 8 | - Node.js 20.19+ or 22.12+ (recommended) 9 | - npm 10 | 11 | ### Getting Started 12 | 13 | 1. **Install dependencies**: 14 | ```bash 15 | npm install 16 | ``` 17 | 18 | 2. **Start development server**: 19 | ```bash 20 | npm run dev 21 | ``` 22 | 23 | The dev server will start at `http://localhost:5173` and proxy `/spector` requests to your .NET application (assumed to be running on `http://localhost:5000`). 24 | 25 | 3. **Make sure your .NET application is running**: 26 | ```bash 27 | cd .. 28 | dotnet run 29 | ``` 30 | 31 | ### Building for Production 32 | 33 | ```bash 34 | npm run build 35 | ``` 36 | 37 | This will compile the React app and output the production build to `../wwwroot/`. 38 | 39 | ## Project Structure 40 | 41 | ``` 42 | ui-src/ 43 | ├── src/ 44 | │ ├── components/ # React components 45 | │ │ ├── Header/ # Header with connection status 46 | │ │ ├── Sidebar/ # Filters and statistics 47 | │ │ ├── TraceList/ # Trace groups and activities 48 | │ │ ├── DetailsPanel/ # Activity details panel 49 | │ │ └── common/ # Reusable components 50 | │ ├── context/ # React Context for state management 51 | │ ├── hooks/ # Custom React hooks (SSE, etc.) 52 | │ ├── types/ # TypeScript type definitions 53 | │ ├── utils/ # Utility functions 54 | │ ├── styles/ # Global styles and CSS variables 55 | │ ├── App.tsx # Main app component 56 | │ └── main.tsx # Entry point 57 | ├── index.html # HTML template 58 | ├── vite.config.ts # Vite configuration 59 | ├── tsconfig.json # TypeScript configuration 60 | └── package.json # Dependencies and scripts 61 | ``` 62 | 63 | ## Key Features 64 | 65 | - **Real-time Updates**: SSE connection with auto-reconnect 66 | - **Component-Based**: Modular React components with TypeScript 67 | - **CSS Modules**: Scoped styling for each component 68 | - **State Management**: React Context for global state 69 | - **Type Safety**: Full TypeScript support 70 | 71 | ## Development Workflow 72 | 73 | 1. Make changes to React components in `src/` 74 | 2. Vite will hot-reload your changes automatically 75 | 3. Test with your .NET application running 76 | 4. Build for production when ready 77 | 78 | ## Build Integration 79 | 80 | The React app is automatically built when you build the .NET project: 81 | 82 | ```bash 83 | cd .. 84 | dotnet build 85 | ``` 86 | 87 | This will: 88 | 1. Run `npm install` (if needed) 89 | 2. Run `npm run build` 90 | 3. Embed the output in the .NET assembly 91 | 92 | ## Troubleshooting 93 | 94 | ### Port Conflicts 95 | If port 5173 is already in use, you can change it in `vite.config.ts`: 96 | ```typescript 97 | server: { 98 | port: 3000, // Change to your preferred port 99 | // ... 100 | } 101 | ``` 102 | 103 | ### Proxy Issues 104 | If the SSE endpoint is on a different port, update the proxy target in `vite.config.ts`: 105 | ```typescript 106 | proxy: { 107 | '/spector': { 108 | target: 'http://localhost:YOUR_PORT', 109 | changeOrigin: true 110 | } 111 | } 112 | ``` 113 | 114 | ### Node Version Warning 115 | If you see a Node.js version warning, it's safe to ignore. The app will still work with Node.js 20.15.1+, though 20.19+ is recommended. 116 | -------------------------------------------------------------------------------- /src/Spector/Handlers/SpectorHttpHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Text; 3 | using System.Text.Json; 4 | using Microsoft.Extensions.Logging; 5 | using Spector.Config; 6 | 7 | namespace Spector.Handlers; 8 | 9 | public class SpectorHttpHandler : DelegatingHandler 10 | { 11 | private readonly ActivitySource _activitySource; 12 | private readonly ILogger _logger; 13 | 14 | public SpectorHttpHandler(ActivitySource activitySource, SpectorOptions opts, ILogger logger) 15 | { 16 | _activitySource = activitySource; 17 | _logger = logger; 18 | } 19 | 20 | protected override async Task SendAsync(HttpRequestMessage request, 21 | CancellationToken cancellationToken) 22 | { 23 | using var activity = _activitySource.StartActivity("HttpOut"); 24 | 25 | if (activity != null) 26 | { 27 | activity.AddTag("spector.type", "http"); 28 | activity.AddTag("spector.url", request.RequestUri?.ToString()); 29 | activity.AddTag("spector.method", request.Method.ToString()); 30 | 31 | // Capture request body if present 32 | if (request.Content != null) 33 | { 34 | var requestBody = await request.Content.ReadAsStringAsync(cancellationToken); 35 | if (!string.IsNullOrEmpty(requestBody)) 36 | { 37 | activity.AddTag("spector.requestBody", requestBody); 38 | } 39 | 40 | // Reset the content stream so it can be read again 41 | if (request.Content is StringContent || request.Content is ByteArrayContent) 42 | { 43 | // For StringContent and ByteArrayContent, we need to recreate it 44 | var contentType = request.Content.Headers.ContentType; 45 | request.Content = new StringContent(requestBody, Encoding.UTF8, 46 | contentType?.MediaType ?? "application/json"); 47 | } 48 | } 49 | } 50 | 51 | HttpResponseMessage response; 52 | 53 | try 54 | { 55 | // Send the request 56 | response = await base.SendAsync(request, cancellationToken); 57 | 58 | if (activity != null) 59 | { 60 | // Add response details to activity 61 | activity.AddTag("spector.status", ((int)response.StatusCode).ToString()); 62 | 63 | // Capture response body 64 | if (response.Content != null) 65 | { 66 | var responseBody = await response.Content.ReadAsStringAsync(cancellationToken); 67 | if (!string.IsNullOrEmpty(responseBody)) 68 | { 69 | activity.AddTag("spector.responseBody", responseBody); 70 | } 71 | 72 | // Reset the content stream so it can be read by the caller 73 | var contentBytes = Encoding.UTF8.GetBytes(responseBody); 74 | response.Content = new ByteArrayContent(contentBytes); 75 | response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue( 76 | response.Content.Headers.ContentType?.MediaType ?? "application/json"); 77 | } 78 | } 79 | } 80 | catch (Exception ex) 81 | { 82 | if (activity != null) 83 | { 84 | activity.AddTag("spector.status", "500"); 85 | activity.AddTag("spector.responseBody", JsonSerializer.Serialize(new { message = ex.Message })); 86 | } 87 | 88 | throw; 89 | } 90 | 91 | return response; 92 | } 93 | } -------------------------------------------------------------------------------- /src/Spector/Service/ActivityCollectorService.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Threading.Channels; 3 | using Microsoft.Extensions.Hosting; 4 | using Microsoft.Extensions.Logging; 5 | using Spector.Config; 6 | using Spector.Models; 7 | using Spector.Storage; 8 | 9 | namespace Spector.Service; 10 | 11 | public class ActivityCollectorService : BackgroundService 12 | { 13 | private readonly Channel _channel; 14 | private readonly ActivityListener _listener; 15 | private readonly InMemoryTraceStore _store; 16 | private readonly ILogger _logger; 17 | private readonly SpectorOptions _opts; 18 | 19 | public ActivityCollectorService(InMemoryTraceStore store, ILogger logger, SpectorOptions opts) 20 | { 21 | _store = store; 22 | _logger = logger; 23 | _opts = opts; 24 | 25 | var options = new BoundedChannelOptions(opts.CollectorChannelCapacity) 26 | { 27 | SingleReader = true, 28 | SingleWriter = false, 29 | FullMode = BoundedChannelFullMode.DropOldest 30 | }; 31 | 32 | _channel = Channel.CreateBounded(options); 33 | 34 | _listener = new ActivityListener 35 | { 36 | ShouldListenTo = src => src.Name == opts.ActivitySourceName, 37 | Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, 38 | ActivityStarted = activity => { /* cheap */ }, 39 | ActivityStopped = activity => 40 | { 41 | // Best practice: map needed fields immediately rather than holding Activity instance. 42 | // But we will enqueue the Activity reference for quick mapping in the consumer. 43 | if (!_channel.Writer.TryWrite(activity)) 44 | { 45 | // drop — keep diagnostic info limited 46 | } 47 | } 48 | }; 49 | 50 | ActivitySource.AddActivityListener(_listener); 51 | } 52 | 53 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 54 | { 55 | await foreach (var activity in _channel.Reader.ReadAllAsync(stoppingToken)) 56 | { 57 | try 58 | { 59 | if (activity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn") 60 | continue; 61 | var dto = MapActivity(activity); 62 | _store.Add(dto); 63 | } 64 | catch (Exception ex) 65 | { 66 | _logger.LogError(ex, "Failed to map or store activity"); 67 | } 68 | } 69 | } 70 | 71 | private TraceDto MapActivity(Activity a) 72 | { 73 | var tags = new Dictionary(); 74 | foreach (var t in a.Tags) 75 | { 76 | if(t.Key.Contains("spector")) 77 | tags[t.Key] = t.Value; 78 | } 79 | 80 | var eventsList = new List(); 81 | foreach (var ev in a.Events) 82 | { 83 | var et = new Dictionary(); 84 | foreach (var kv in ev.Tags) et[kv.Key] = kv.Value?.ToString() ?? ""; 85 | eventsList.Add(new TraceEventDto { Name = ev.Name, Timestamp = ev.Timestamp, Tags = et }); 86 | } 87 | 88 | return new TraceDto 89 | { 90 | Name = a.DisplayName ?? string.Empty, 91 | TraceId = a.TraceId.ToString(), 92 | SpanId = a.SpanId.ToString(), 93 | ParentSpanId = a.ParentSpanId.ToString(), 94 | StartTimeUtc = a.StartTimeUtc, 95 | Duration = a.Duration, 96 | Kind = a.Kind.ToString(), 97 | Tags = tags, 98 | Events = eventsList 99 | }; 100 | } 101 | } -------------------------------------------------------------------------------- /src/Spector/ui-src/src/components/TraceList/ActivityItem.module.css: -------------------------------------------------------------------------------- 1 | .activityItem { 2 | display: flex; 3 | align-items: center; 4 | padding: 0.5rem 0.75rem; 5 | margin: 0.125rem 0; 6 | background: var(--bg-primary); 7 | border: 1px solid var(--border-color); 8 | border-radius: 4px; 9 | cursor: pointer; 10 | transition: all 0.2s; 11 | position: relative; 12 | } 13 | 14 | .activityItem:hover { 15 | background: var(--bg-tertiary); 16 | border-color: var(--accent-blue); 17 | transform: translateX(2px); 18 | } 19 | 20 | .activityItem.selected { 21 | background: var(--bg-tertiary); 22 | border-color: var(--accent-blue); 23 | box-shadow: 0 0 0 1px rgba(74, 158, 255, 0.2); 24 | } 25 | 26 | .activityItem.child { 27 | margin-left: 1.5rem; 28 | } 29 | 30 | .activityItem.child::before { 31 | content: ''; 32 | position: absolute; 33 | left: -0.75rem; 34 | top: 50%; 35 | width: 0.75rem; 36 | height: 1px; 37 | background: var(--border-color); 38 | } 39 | 40 | .activityType { 41 | width: 6px; 42 | height: 6px; 43 | border-radius: 50%; 44 | margin-right: 0.625rem; 45 | flex-shrink: 0; 46 | } 47 | 48 | .activityType.httpin { 49 | background: var(--http-in); 50 | box-shadow: 0 0 6px var(--http-in); 51 | } 52 | 53 | .activityType.httpout { 54 | background: var(--http-out); 55 | box-shadow: 0 0 6px var(--http-out); 56 | } 57 | 58 | .activityContent { 59 | flex: 1; 60 | min-width: 0; 61 | } 62 | 63 | .activityName { 64 | font-size: 0.75rem; 65 | font-weight: 600; 66 | margin-bottom: 0.125rem; 67 | display: flex; 68 | align-items: center; 69 | gap: 0.375rem; 70 | } 71 | 72 | .activityMethod { 73 | font-size: 0.6875rem; 74 | padding: 0.0625rem 0.375rem; 75 | border-radius: 3px; 76 | font-weight: 600; 77 | background: var(--bg-tertiary); 78 | } 79 | 80 | .methodGET { 81 | color: var(--accent-green); 82 | } 83 | 84 | .methodPOST { 85 | color: var(--accent-blue); 86 | } 87 | 88 | .methodPUT { 89 | color: var(--accent-orange); 90 | } 91 | 92 | .methodDELETE { 93 | color: var(--accent-red); 94 | } 95 | 96 | .activityStatus { 97 | font-size: 0.6875rem; 98 | padding: 0.0625rem 0.375rem; 99 | border-radius: 3px; 100 | font-weight: 600; 101 | } 102 | 103 | .statusSuccess { 104 | background: rgba(52, 211, 153, 0.15); 105 | color: var(--accent-green); 106 | } 107 | 108 | .statusRedirect { 109 | background: rgba(74, 158, 255, 0.15); 110 | color: var(--accent-blue); 111 | } 112 | 113 | .statusClientError { 114 | background: rgba(251, 146, 60, 0.15); 115 | color: var(--accent-orange); 116 | } 117 | 118 | .statusServerError { 119 | background: rgba(248, 113, 113, 0.15); 120 | color: var(--accent-red); 121 | } 122 | 123 | .statusError { 124 | background: rgba(248, 113, 113, 0.2); 125 | color: var(--accent-red); 126 | font-weight: 700; 127 | } 128 | 129 | .activityUrl { 130 | font-size: 0.6875rem; 131 | color: var(--text-secondary); 132 | white-space: nowrap; 133 | overflow: hidden; 134 | text-overflow: ellipsis; 135 | } 136 | 137 | .activityTiming { 138 | display: flex; 139 | flex-direction: column; 140 | align-items: flex-end; 141 | gap: 0.25rem; 142 | margin-left: 0.5rem; 143 | } 144 | 145 | .activityDuration { 146 | font-size: 0.6875rem; 147 | font-weight: 600; 148 | color: var(--accent-orange); 149 | } 150 | 151 | .activityTimeline { 152 | width: 80px; 153 | height: 3px; 154 | background: var(--bg-tertiary); 155 | border-radius: 2px; 156 | overflow: hidden; 157 | position: relative; 158 | } 159 | 160 | .timelineBar { 161 | height: 100%; 162 | background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple)); 163 | border-radius: 2px; 164 | transition: width 0.3s; 165 | } -------------------------------------------------------------------------------- /src/Spector/ui-src/src/components/TraceList/ActivityItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Activity } from '../../types'; 3 | import { useSpector } from '../../context/SpectorContext'; 4 | import { parseDuration, formatDuration } from '../../utils/duration'; 5 | import styles from './ActivityItem.module.css'; 6 | 7 | interface ActivityItemProps { 8 | activity: Activity; 9 | children?: Activity[]; 10 | traceStartTime: Date; 11 | traceDuration: number; 12 | isChild?: boolean; 13 | } 14 | 15 | export function ActivityItem({ 16 | activity, 17 | children = [], 18 | traceStartTime, 19 | traceDuration, 20 | isChild = false 21 | }: ActivityItemProps) { 22 | const { selectActivity, selectedActivity, filters } = useSpector(); 23 | 24 | const type = activity.Name.toLowerCase(); 25 | const method = activity.Tags['spector.method'] || 'N/A'; 26 | const url = activity.Tags['spector.url'] || 'N/A'; 27 | const status = activity.Tags['spector.status'] || ''; 28 | const error = activity.Tags['spector.error'] || ''; 29 | const duration = parseDuration(activity.Duration); 30 | 31 | // Check filters 32 | if (type === 'httpin' && !filters.httpIn) return null; 33 | if (type === 'httpout' && !filters.httpOut) return null; 34 | 35 | // Calculate timeline position and width 36 | const timelineWidth = traceDuration > 0 ? (duration / traceDuration) * 100 : 100; 37 | 38 | // Determine status color 39 | let statusClass = ''; 40 | let statusDisplay = status; 41 | 42 | if (error || status === '0') { 43 | statusClass = styles.statusError; 44 | statusDisplay = 'ERROR'; 45 | } else if (status) { 46 | const statusCode = parseInt(status); 47 | if (statusCode >= 200 && statusCode < 300) statusClass = styles.statusSuccess; 48 | else if (statusCode >= 300 && statusCode < 400) statusClass = styles.statusRedirect; 49 | else if (statusCode >= 400 && statusCode < 500) statusClass = styles.statusClientError; 50 | else if (statusCode >= 500) statusClass = styles.statusServerError; 51 | } 52 | 53 | const isSelected = selectedActivity?.SpanId === activity.SpanId; 54 | 55 | const handleClick = (e: React.MouseEvent) => { 56 | e.stopPropagation(); 57 | selectActivity(activity.SpanId); 58 | }; 59 | 60 | return ( 61 | <> 62 |
66 | 67 |
68 |
69 | {method} 70 | {status && {statusDisplay}} 71 |
72 |
{url}
73 |
74 |
75 | {formatDuration(duration)} 76 |
77 |
78 |
79 |
80 |
81 | {children.map(child => ( 82 | 90 | ))} 91 | 92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /src/Spector/ui-src/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Spector/Middleware/HttpActivityMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Http.Extensions; 4 | using Spector.Config; 5 | 6 | namespace Spector.Middleware; 7 | public class HttpActivityMiddleware 8 | { 9 | private readonly RequestDelegate _next; 10 | private readonly ActivitySource _activitySource; 11 | private readonly SpectorOptions _opts; 12 | 13 | public HttpActivityMiddleware(RequestDelegate next,ActivitySource activitySource, SpectorOptions opts) 14 | { 15 | _activitySource = activitySource; 16 | _opts = opts; 17 | _next = next; 18 | } 19 | 20 | public async Task InvokeAsync(HttpContext context) 21 | { 22 | if (context.Request.Path.StartsWithSegments("/spector")) 23 | { 24 | await _next(context); 25 | return; 26 | } 27 | using var activity = _activitySource.StartActivity("HttpIn"); 28 | 29 | if (activity != null) 30 | { 31 | string? requestBody = null; 32 | string? responseBody = null; 33 | 34 | try 35 | { 36 | // Enable request body buffering to allow multiple reads 37 | if (_opts.RecordRequestBodies) 38 | { 39 | context.Request.EnableBuffering(); 40 | 41 | // Read the request body 42 | using var reader = new StreamReader(context.Request.Body, leaveOpen: true); 43 | requestBody = await reader.ReadToEndAsync(); 44 | 45 | // Reset the stream position for the next middleware 46 | context.Request.Body.Position = 0; 47 | } 48 | 49 | // Capture response body by replacing the response stream 50 | Stream originalResponseBody = context.Response.Body; 51 | 52 | if (_opts.RecordResponseBodies) 53 | { 54 | using var responseBodyStream = new MemoryStream(); 55 | context.Response.Body = responseBodyStream; 56 | 57 | try 58 | { 59 | await _next(context); 60 | 61 | // Read the response body 62 | responseBodyStream.Position = 0; 63 | using var reader = new StreamReader(responseBodyStream); 64 | responseBody = await reader.ReadToEndAsync(); 65 | 66 | // Copy the response back to the original stream 67 | responseBodyStream.Position = 0; 68 | await responseBodyStream.CopyToAsync(originalResponseBody); 69 | } 70 | finally 71 | { 72 | context.Response.Body = originalResponseBody; 73 | } 74 | } 75 | else 76 | { 77 | await _next(context); 78 | } 79 | 80 | // Add tags after processing 81 | activity.AddTag("spector.type", "http"); 82 | activity.AddTag("spector.url", context.Request.Path); 83 | activity.AddTag("spector.method", context.Request.Method); 84 | 85 | if (_opts.RecordRequestBodies && requestBody != null) 86 | { 87 | activity.AddTag("spector.requestBody", requestBody); 88 | } 89 | 90 | if (_opts.RecordResponseBodies && responseBody != null) 91 | { 92 | activity.AddTag("spector.responseBody", responseBody); 93 | } 94 | 95 | activity.AddTag("spector.status", context.Response.StatusCode.ToString()); 96 | } 97 | catch (Exception ex) 98 | { 99 | throw; 100 | } 101 | } 102 | else 103 | { 104 | await _next(context); 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /src/Spector/README.md: -------------------------------------------------------------------------------- 1 | # Spector 2 | 3 | A lightweight network and dependency inspector for ASP.NET Core applications. Spector captures HTTP traces (incoming and outgoing requests) and provides a real-time web UI for monitoring and debugging. 4 | 5 | ## Features 6 | 7 | - 🔍 **HTTP Request Tracing** - Automatically captures incoming and outgoing HTTP requests 8 | - 📊 **Real-time Monitoring** - Live updates via Server-Sent Events (SSE) 9 | - 🎨 **Embedded Web UI** - Beautiful, responsive interface for viewing traces 10 | - 🔗 **Dependency Tracking** - Visualize request/response chains and dependencies 11 | - 📦 **Zero Configuration** - Works out of the box with sensible defaults 12 | - 🚀 **Lightweight** - Minimal performance overhead 13 | 14 | ## Installation 15 | 16 | Install the Spector NuGet package: 17 | 18 | ```bash 19 | dotnet add package Spector 20 | ``` 21 | 22 | Or via Package Manager Console: 23 | 24 | ```powershell 25 | Install-Package Spector 26 | ``` 27 | 28 | ## Quick Start 29 | 30 | ### 1. Add Spector to your services 31 | 32 | In your `Program.cs` or `Startup.cs`: 33 | 34 | ```csharp 35 | using Spector; 36 | 37 | var builder = WebApplication.CreateBuilder(args); 38 | 39 | // Add Spector services 40 | builder.Services.AddSpector(); 41 | 42 | // ... other service registrations 43 | 44 | var app = builder.Build(); 45 | 46 | // Use Spector middleware 47 | app.UseSpector(); 48 | 49 | // ... other middleware 50 | 51 | app.Run(); 52 | ``` 53 | 54 | ### 2. Access the UI 55 | 56 | Run your application and navigate to: 57 | 58 | ``` 59 | http://localhost:/local-insights 60 | ``` 61 | 62 | You'll see a real-time dashboard showing all HTTP traces captured by your application. 63 | 64 | ## Configuration 65 | 66 | Spector works with zero configuration, but you can customize it if needed: 67 | 68 | ```csharp 69 | builder.Services.AddSpector(); 70 | ``` 71 | 72 | ### Available Options 73 | 74 | The `SpectorOptions` class provides the following configuration: 75 | 76 | - **`UiPath`** - Custom path for the UI (default: `/local-insights`) 77 | - **`SseEndpoint`** - Custom SSE endpoint path (default: `/local-insights/events`) 78 | - **`ActivitySourceName`** - Activity source name for tracing (default: `Spector`) 79 | - **`InMemoryMaxTraces`** - Maximum number of traces to keep in memory (default: 100) 80 | 81 | To customize options, modify the `SpectorOptions` instance after registration: 82 | 83 | ```csharp 84 | builder.Services.AddSpector(); 85 | builder.Services.Configure(options => 86 | { 87 | options.UiPath = "/my-custom-path"; 88 | options.InMemoryMaxTraces = 200; 89 | }); 90 | ``` 91 | 92 | ## How It Works 93 | 94 | Spector uses ASP.NET Core's built-in diagnostics features: 95 | 96 | 1. **Activity Tracing** - Leverages `System.Diagnostics.Activity` for distributed tracing 97 | 2. **Middleware** - Captures incoming HTTP requests via middleware 98 | 3. **HTTP Handler** - Intercepts outgoing HTTP calls via `IHttpMessageHandlerBuilderFilter` 99 | 4. **In-Memory Storage** - Stores recent traces in memory for quick access 100 | 5. **SSE Streaming** - Pushes updates to the UI in real-time 101 | 102 | ## What Gets Captured 103 | 104 | For each HTTP request/response, Spector captures: 105 | 106 | - **Request Details** 107 | - HTTP method (GET, POST, etc.) 108 | - Full URL 109 | - Headers 110 | - Request body (when available) 111 | - Timestamp 112 | 113 | - **Response Details** 114 | - Status code 115 | - Headers 116 | - Response body (when available) 117 | - Duration 118 | 119 | - **Trace Context** 120 | - Trace ID 121 | - Span ID 122 | - Parent-child relationships 123 | 124 | ## Use Cases 125 | 126 | - **Development** - Debug API calls and inspect request/response data 127 | - **Testing** - Verify HTTP interactions during integration tests 128 | - **Troubleshooting** - Identify slow dependencies or failing requests 129 | - **Learning** - Understand how your application communicates with external services 130 | 131 | ## Requirements 132 | 133 | - .NET 8.0 or later 134 | - ASP.NET Core application 135 | 136 | ## License 137 | 138 | MIT License - see LICENSE file for details 139 | 140 | ## Contributing 141 | 142 | Contributions are welcome! Please feel free to submit issues or pull requests. 143 | 144 | ## Support 145 | 146 | For issues, questions, or feature requests, please open an issue on GitHub. 147 | -------------------------------------------------------------------------------- /src/Spector/ui-src/src/context/SpectorContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState, useCallback, type ReactNode } from 'react'; 2 | import type { Activity, Trace, Filters, ConnectionStatus } from '../types'; 3 | import { parseDuration } from '../utils/duration'; 4 | 5 | interface SpectorState { 6 | traces: Map; 7 | activities: Map; 8 | filters: Filters; 9 | isPaused: boolean; 10 | selectedActivity: Activity | null; 11 | connectionStatus: ConnectionStatus; 12 | } 13 | 14 | interface SpectorContextType extends SpectorState { 15 | addActivity: (activity: Activity) => void; 16 | clearAll: () => void; 17 | togglePause: () => void; 18 | selectActivity: (spanId: string) => void; 19 | closeDetails: () => void; 20 | setFilter: (filter: keyof Filters, value: boolean) => void; 21 | setConnectionStatus: (status: ConnectionStatus) => void; 22 | } 23 | 24 | const SpectorContext = createContext(undefined); 25 | 26 | export function SpectorProvider({ children }: { children: ReactNode }) { 27 | const [traces, setTraces] = useState>(new Map()); 28 | const [activities, setActivities] = useState>(new Map()); 29 | const [filters, setFilters] = useState({ httpIn: true, httpOut: true }); 30 | const [isPaused, setIsPaused] = useState(false); 31 | const [selectedActivity, setSelectedActivity] = useState(null); 32 | const [connectionStatus, setConnectionStatus] = useState('connecting'); 33 | 34 | const addActivity = useCallback((activity: Activity) => { 35 | const { TraceId, SpanId } = activity; 36 | 37 | // Store activity 38 | setActivities(prev => new Map(prev).set(SpanId, activity)); 39 | 40 | // Get or create trace group 41 | setTraces(prev => { 42 | const newTraces = new Map(prev); 43 | 44 | if (!newTraces.has(TraceId)) { 45 | newTraces.set(TraceId, { 46 | traceId: TraceId, 47 | activities: [], 48 | startTime: new Date(activity.StartTimeUtc), 49 | endTime: new Date(activity.StartTimeUtc) 50 | }); 51 | } 52 | 53 | const trace = newTraces.get(TraceId)!; 54 | trace.activities.push(activity); 55 | 56 | // Update trace timing 57 | const activityStart = new Date(activity.StartTimeUtc); 58 | const activityEnd = new Date(activityStart.getTime() + parseDuration(activity.Duration)); 59 | 60 | if (activityStart < trace.startTime) trace.startTime = activityStart; 61 | if (activityEnd > trace.endTime) trace.endTime = activityEnd; 62 | 63 | return newTraces; 64 | }); 65 | }, []); 66 | 67 | const clearAll = useCallback(() => { 68 | setTraces(new Map()); 69 | setActivities(new Map()); 70 | setSelectedActivity(null); 71 | }, []); 72 | 73 | const togglePause = useCallback(() => { 74 | setIsPaused(prev => !prev); 75 | }, []); 76 | 77 | const selectActivity = useCallback((spanId: string) => { 78 | setActivities(prev => { 79 | const activity = prev.get(spanId); 80 | if (activity) { 81 | setSelectedActivity(activity); 82 | } 83 | return prev; 84 | }); 85 | }, []); 86 | 87 | const closeDetails = useCallback(() => { 88 | setSelectedActivity(null); 89 | }, []); 90 | 91 | const setFilter = useCallback((filter: keyof Filters, value: boolean) => { 92 | setFilters(prev => ({ ...prev, [filter]: value })); 93 | }, []); 94 | 95 | const value: SpectorContextType = { 96 | traces, 97 | activities, 98 | filters, 99 | isPaused, 100 | selectedActivity, 101 | connectionStatus, 102 | addActivity, 103 | clearAll, 104 | togglePause, 105 | selectActivity, 106 | closeDetails, 107 | setFilter, 108 | setConnectionStatus 109 | }; 110 | 111 | return {children}; 112 | } 113 | 114 | export function useSpector() { 115 | const context = useContext(SpectorContext); 116 | if (context === undefined) { 117 | throw new Error('useSpector must be used within a SpectorProvider'); 118 | } 119 | return context; 120 | } 121 | -------------------------------------------------------------------------------- /src/Spector/SpectorExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Reflection; 3 | using System.Text.Json; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.FileProviders; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.Extensions.DependencyInjection.Extensions; 9 | using Microsoft.Extensions.Http; 10 | using Spector.Config; 11 | using Spector.Handlers; 12 | using Spector.Middleware; 13 | using Spector.Service; 14 | using Spector.Storage; 15 | 16 | namespace Spector; 17 | 18 | public static class SpectorExtensions 19 | { 20 | public static IServiceCollection AddSpector(this IServiceCollection services) 21 | { 22 | var opts = new SpectorOptions(); 23 | services.AddSingleton(opts); 24 | 25 | services.AddSingleton(sp => new InMemoryTraceStore(opts.InMemoryMaxTraces)); 26 | services.AddHostedService(); 27 | services.AddSingleton(new ActivitySource(opts.ActivitySourceName)); 28 | services.AddTransient(); 29 | services.TryAddEnumerable(ServiceDescriptor.Singleton()); 30 | 31 | return services; 32 | } 33 | 34 | public static IApplicationBuilder UseSpector(this IApplicationBuilder app) 35 | { 36 | app.UseMiddleware(); 37 | 38 | var opts = app.ApplicationServices.GetRequiredService(); 39 | var store = app.ApplicationServices.GetRequiredService(); 40 | 41 | MapSpectorSseEndpoint(app, opts, store); 42 | MapSpectorUiEndpoint(app, opts); 43 | return app; 44 | } 45 | 46 | private static void MapSpectorUiEndpoint(this IApplicationBuilder app, SpectorOptions opts) 47 | { 48 | var assembly = Assembly.GetExecutingAssembly(); 49 | IFileProvider? embeddedProvider = null; 50 | var asmNs = assembly.GetName().Name ?? string.Empty; 51 | var tryPaths = new[] { $"{asmNs}.wwwroot", "wwwroot", $"{asmNs}" }; 52 | 53 | foreach (var root in tryPaths) 54 | { 55 | var provider = new EmbeddedFileProvider(assembly, root); 56 | // quick check: see if index file exists 57 | var info = provider.GetFileInfo("index.html"); 58 | if (info != null && info.Exists) 59 | { 60 | embeddedProvider = provider; 61 | break; 62 | } 63 | } 64 | 65 | if (embeddedProvider == null) 66 | { 67 | // fallback: attempt non-rooted provider 68 | embeddedProvider = new EmbeddedFileProvider(assembly); 69 | } 70 | 71 | // Map the UI at opts.UiPath (e.g. "/local-insights") 72 | var uiPath = opts.UiPath?.TrimEnd('/') ?? "/local-insights"; 73 | 74 | app.Map(uiPath, branch => 75 | { 76 | // serve default file when hitting /local-insights 77 | var defaultOpts = new DefaultFilesOptions { FileProvider = embeddedProvider, RequestPath = "" }; 78 | defaultOpts.DefaultFileNames.Clear(); 79 | defaultOpts.DefaultFileNames.Add("index.html"); 80 | branch.UseDefaultFiles(defaultOpts); 81 | 82 | branch.UseStaticFiles(new StaticFileOptions 83 | { 84 | FileProvider = embeddedProvider, 85 | RequestPath = "" 86 | }); 87 | 88 | // If someone hits the folder root, and default files didn't pick it up, ensure index is returned 89 | branch.Run(async ctx => 90 | { 91 | var file = embeddedProvider.GetFileInfo("local-insights.html"); 92 | if (file.Exists) 93 | { 94 | ctx.Response.ContentType = "text/html; charset=utf-8"; 95 | using var stream = file.CreateReadStream(); 96 | await stream.CopyToAsync(ctx.Response.Body); 97 | } 98 | else 99 | { 100 | ctx.Response.StatusCode = 404; 101 | await ctx.Response.WriteAsync("Local Insights UI not found in assembly."); 102 | } 103 | }); 104 | }); 105 | } 106 | 107 | private static void MapSpectorSseEndpoint(this IApplicationBuilder app, SpectorOptions opts, InMemoryTraceStore store) 108 | { 109 | var ssePath = opts.SseEndpoint ?? "/local-insights/events"; 110 | app.Map(ssePath, branch => 111 | { 112 | branch.Run(async ctx => 113 | { 114 | ctx.Response.Headers.Add("Content-Type", "text/event-stream"); 115 | 116 | var lastIndex = -1; 117 | while (!ctx.RequestAborted.IsCancellationRequested) 118 | { 119 | var items = store.GetAll(); 120 | if (items.Count - 1 > lastIndex) 121 | { 122 | for (int i = lastIndex + 1; i < items.Count; i++) 123 | { 124 | var json = JsonSerializer.Serialize(items[i]); 125 | await ctx.Response.WriteAsync($"data: {json}\n\n"); 126 | await ctx.Response.Body.FlushAsync(ctx.RequestAborted); 127 | } 128 | lastIndex = items.Count - 1; 129 | } 130 | 131 | try { await Task.Delay(300, ctx.RequestAborted); } catch { break; } 132 | } 133 | }); 134 | }); 135 | } 136 | } -------------------------------------------------------------------------------- /src/Spector/ui-src/src/components/DetailsPanel/DetailsPanel.tsx: -------------------------------------------------------------------------------- 1 | import { useSpector } from '../../context/SpectorContext'; 2 | import { parseDuration, formatDuration } from '../../utils/duration'; 3 | import { formatJson } from '../../utils/formatting'; 4 | import { Badge } from '../common/Badge'; 5 | import styles from './DetailsPanel.module.css'; 6 | 7 | export function DetailsPanel() { 8 | const { selectedActivity, closeDetails } = useSpector(); 9 | 10 | if (!selectedActivity) { 11 | return ( 12 | 20 | ); 21 | } 22 | 23 | const type = selectedActivity.Name.toLowerCase() as 'httpin' | 'httpout'; 24 | const method = selectedActivity.Tags['spector.method'] || 'N/A'; 25 | const url = selectedActivity.Tags['spector.url'] || 'N/A'; 26 | const status = selectedActivity.Tags['spector.status'] || ''; 27 | const error = selectedActivity.Tags['spector.error'] || ''; 28 | const errorType = selectedActivity.Tags['spector.errorType'] || ''; 29 | const duration = parseDuration(selectedActivity.Duration); 30 | const requestBody = selectedActivity.Tags['spector.requestBody'] || ''; 31 | const responseBody = selectedActivity.Tags['spector.responseBody'] || ''; 32 | 33 | return ( 34 | 117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spector 2 | 3 | 4 | **A lightweight network and dependency inspector for ASP.NET Core applications** 5 | 6 | [![NuGet](https://img.shields.io/nuget/v/Spector.svg)](https://www.nuget.org/packages/Spector/) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 8 | [![.NET](https://img.shields.io/badge/.NET-8.0-purple.svg)](https://dotnet.microsoft.com/download) 9 | 10 | [Features](#features) • [Installation](#installation) • [Quick Start](#quick-start) • [Documentation](#documentation) • [Contributing](#contributing) 11 | 12 | 13 | --- 14 | 15 | ## 🎯 Overview 16 | 17 | Spector is a powerful yet lightweight debugging tool for ASP.NET Core applications that captures and visualizes HTTP traces in real-time. Monitor incoming requests, outgoing HTTP calls, and their dependencies through a beautiful embedded web interface—all with zero configuration required. 18 | 19 | Perfect for development, testing, and troubleshooting API interactions without the overhead of external monitoring tools. 20 | 21 | ## ✨ Features 22 | 23 | - 🔍 **Automatic HTTP Tracing** - Captures all incoming and outgoing HTTP requests automatically 24 | - 📊 **Real-time Monitoring** - Live updates via Server-Sent Events (SSE) 25 | - 🎨 **Beautiful Web UI** - Modern React-based interface embedded directly in your application 26 | - 🔗 **Dependency Visualization** - See the complete request/response chain with parent-child relationships 27 | - 📦 **Zero Configuration** - Works out of the box with sensible defaults 28 | - 🚀 **Lightweight** - Minimal performance overhead, in-memory storage 29 | - 🔐 **Development-Focused** - Designed for local development and testing environments 30 | - 📝 **Detailed Inspection** - View headers, bodies, status codes, and timing information 31 | 32 | ## 📸 Screenshots 33 | 34 | ![Spector UI](assets/image.png) 35 | *Real-time HTTP trace monitoring with detailed request/response inspection* 36 | 37 | ## 🚀 Installation 38 | 39 | Install the Spector NuGet package: 40 | 41 | ```bash 42 | dotnet add package Spector 43 | ``` 44 | 45 | Or via Package Manager Console: 46 | 47 | ```powershell 48 | Install-Package Spector 49 | ``` 50 | 51 | ## 🏁 Quick Start 52 | 53 | ### 1. Add Spector to your ASP.NET Core application 54 | 55 | In your `Program.cs`: 56 | 57 | ```csharp 58 | using Spector; 59 | 60 | var builder = WebApplication.CreateBuilder(args); 61 | 62 | // Add Spector services 63 | builder.Services.AddSpector(); 64 | 65 | // ... your other service registrations 66 | builder.Services.AddControllers(); 67 | builder.Services.AddHttpClient(); 68 | 69 | var app = builder.Build(); 70 | 71 | // Use Spector middleware (add early in the pipeline) 72 | app.UseSpector(); 73 | 74 | // ... your other middleware 75 | app.UseAuthorization(); 76 | app.MapControllers(); 77 | 78 | app.Run(); 79 | ``` 80 | 81 | ### 2. Run your application 82 | 83 | ```bash 84 | dotnet run 85 | ``` 86 | 87 | ### 3. Access the Spector UI 88 | 89 | Navigate to: 90 | 91 | ``` 92 | http://localhost:/spector 93 | ``` 94 | 95 | You'll see a real-time dashboard showing all HTTP traces captured by your application! 🎉 96 | 97 | ## 📚 Documentation 98 | 99 | ### Configuration 100 | 101 | Spector works with zero configuration, but you can customize it: 102 | 103 | ```csharp 104 | builder.Services.AddSpector(); 105 | builder.Services.Configure(options => 106 | { 107 | options.UiPath = "/my-custom-path"; // Default: "/spector" 108 | options.SseEndpoint = "/my-events"; // Default: "/spector/events" 109 | options.InMemoryMaxTraces = 200; // Default: 100 110 | options.ActivitySourceName = "MyApp"; // Default: "Spector" 111 | }); 112 | ``` 113 | 114 | ### What Gets Captured 115 | 116 | For each HTTP request/response, Spector captures: 117 | 118 | **Request Details:** 119 | - HTTP method (GET, POST, PUT, DELETE, etc.) 120 | - Full URL with query parameters 121 | - Request headers 122 | - Request body (when available) 123 | - Timestamp 124 | 125 | **Response Details:** 126 | - HTTP status code 127 | - Response headers 128 | - Response body (when available) 129 | - Duration/timing 130 | 131 | **Trace Context:** 132 | - Trace ID for distributed tracing 133 | - Span ID 134 | - Parent-child relationships between requests 135 | 136 | ### How It Works 137 | 138 | Spector leverages ASP.NET Core's built-in diagnostics infrastructure: 139 | 140 | 1. **Activity Tracing** - Uses `System.Diagnostics.Activity` for distributed tracing 141 | 2. **Middleware** - Captures incoming HTTP requests via custom middleware 142 | 3. **HTTP Message Handler** - Intercepts outgoing HTTP calls using `IHttpMessageHandlerBuilderFilter` 143 | 4. **In-Memory Storage** - Stores recent traces in a circular buffer 144 | 5. **SSE Streaming** - Pushes real-time updates to the web UI 145 | 6. **Embedded UI** - React-based interface bundled as embedded resources 146 | 147 | ## 🎯 Use Cases 148 | 149 | - **Local Development** - Debug API calls and inspect request/response data without external tools 150 | - **Integration Testing** - Verify HTTP interactions during automated tests 151 | - **Troubleshooting** - Identify slow dependencies, failing requests, or unexpected behavior 152 | - **Learning** - Understand how your application communicates with external services 153 | - **API Documentation** - See actual request/response examples for your endpoints 154 | 155 | ## 🛠️ Development 156 | 157 | ### Prerequisites 158 | 159 | - [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) 160 | - [Node.js 18+](https://nodejs.org/) (for UI development) 161 | 162 | ### Building from Source 163 | 164 | ```bash 165 | # Clone the repository 166 | git clone https://github.com/yashwanthkkn/spector.git 167 | cd spector 168 | 169 | # Build the solution 170 | dotnet build 171 | 172 | # Run tests 173 | dotnet test 174 | 175 | # Pack the NuGet package 176 | dotnet pack src/Spector/Spector.csproj -c Release -o ./nupkg 177 | ``` 178 | 179 | ### Project Structure 180 | 181 | ``` 182 | network-inspector/ 183 | ├── src/ 184 | │ └── Spector/ # Main library 185 | │ ├── Config/ # Configuration options 186 | │ ├── Handlers/ # HTTP message handlers 187 | │ ├── Middleware/ # ASP.NET Core middleware 188 | │ ├── Models/ # Data models 189 | │ ├── Service/ # Background services 190 | │ ├── Storage/ # In-memory trace storage 191 | │ └── ui-src/ # React UI source 192 | ├── tests/ 193 | │ └── NetworkInspector.TestApi/ # Test API project 194 | ├── docs/ # Documentation 195 | └── nupkg/ # Built NuGet packages 196 | ``` 197 | 198 | ### UI Development 199 | 200 | The UI is built with React, TypeScript, and Vite: 201 | 202 | ```bash 203 | cd src/Spector/ui-src 204 | 205 | # Install dependencies 206 | npm install 207 | 208 | # Run dev server 209 | npm run dev 210 | 211 | # Build for production 212 | npm run build 213 | ``` 214 | 215 | ## 🤝 Contributing 216 | 217 | Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. 218 | 219 | ### Contribution Guidelines 220 | 221 | 1. Fork the repository 222 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 223 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 224 | 4. Push to the branch (`git push origin feature/AmazingFeature`) 225 | 5. Open a Pull Request 226 | 227 | Please make sure to: 228 | - Update tests as appropriate 229 | - Follow the existing code style 230 | - Update documentation for any new features 231 | 232 | ## 📋 Requirements 233 | 234 | - .NET 8.0 or later 235 | - ASP.NET Core application 236 | - Modern web browser (for viewing the UI) 237 | 238 | ## ⚠️ Important Notes 239 | 240 | - **Development Use Only** - Spector is designed for development and testing environments. Do not use in production as it stores sensitive request/response data in memory. 241 | - **Memory Usage** - Traces are stored in memory with a configurable limit (default: 100 traces) 242 | - **Security** - The UI endpoint is not authenticated. Ensure it's only accessible in trusted environments. 243 | 244 | ## 📝 License 245 | 246 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 247 | 248 | ## 🙏 Acknowledgments 249 | 250 | - Built with [ASP.NET Core](https://dotnet.microsoft.com/apps/aspnet) 251 | - UI powered by [React](https://react.dev/) and [Vite](https://vitejs.dev/) 252 | - Inspired by the need for lightweight, embedded debugging tools 253 | 254 | ## 📞 Support 255 | 256 | - 🐛 **Bug Reports**: [Open an issue](https://github.com/yashwanthkkn/spector/issues) 257 | - 💡 **Feature Requests**: [Open an issue](https://github.com/yashwanthkkn/spector/issues) 258 | - 💬 **Questions**: [Start a discussion](https://github.com/yashwanthkkn/spector/discussions) 259 | 260 | ## 🗺️ Roadmap 261 | 262 | - [ ] Export traces to file (JSON, HAR format) 263 | - [ ] Filter and search capabilities 264 | - [ ] Performance metrics and analytics 265 | - [ ] Custom trace retention policies 266 | - [ ] WebSocket support 267 | - [ ] gRPC call tracing 268 | - [ ] Trace comparison tools 269 | 270 | 271 | **Made with ❤️ by [Yashwanth K](https://github.com/yashwanthkkn)** 272 | 273 | If you find Spector useful, please consider giving it a ⭐ on GitHub! 274 | 275 | --------------------------------------------------------------------------------