diff --git a/.gitignore b/.gitignore
index 1512392..4642540 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,7 @@ _cgo_*
_obj
_test
_testmain.go
+*.protoset
/VERSION.cache
/bin/
@@ -29,7 +30,6 @@ _testmain.go
/misc/cgo/life/run.out
/misc/cgo/stdio/run.out
/misc/cgo/testso/main
-/pkg/
/src/*.*/
/src/cmd/cgo/zdefaultcc.go
/src/cmd/dist/dist
@@ -45,4 +45,4 @@ _testmain.go
/test/garbage/*.out
/test/pass.out
/test/run.out
-/test/times.out
\ No newline at end of file
+/test/times.out
diff --git a/cmd/examples/README.md b/cmd/examples/README.md
index d6d0b5b..4ed99fd 100644
--- a/cmd/examples/README.md
+++ b/cmd/examples/README.md
@@ -6,3 +6,4 @@ This directory contains example applications demonstrating GoVisual's features.
- [Basic Example](basic/) - Simple example demonstrating core GoVisual functionality
- [OpenTelemetry Example](otel/) - Example demonstrating OpenTelemetry integration
+- [gRPC Agent Example](grpc_agent/) - Example demonstrating how to use GoVisual with a gRPC agent
\ No newline at end of file
diff --git a/cmd/examples/grpc_agent/README.md b/cmd/examples/grpc_agent/README.md
new file mode 100644
index 0000000..14a2f24
--- /dev/null
+++ b/cmd/examples/grpc_agent/README.md
@@ -0,0 +1,639 @@
+# GoVisual Agent Architecture
+
+This document explains how to use the new agent architecture in GoVisual to monitor distributed services.
+
+## What is the Agent Architecture?
+
+The agent architecture allows GoVisual to collect request data from multiple services (potentially running on different machines) and visualize them in a central dashboard. This is particularly useful for microservice architectures or distributed systems where multiple services need to be monitored.
+
+### Key Components
+
+1. **Agents**: Lightweight components that attach to services (gRPC, HTTP) to collect request/response data
+2. **Transports**: Mechanisms for sending data from agents to the visualization server
+3. **Visualization Server**: Central server that receives, stores, and displays the request data
+
+## Getting Started
+
+### 1. Setting Up the Visualization Server
+
+Start by initializing a visualization server that will display the dashboard and receive agent data:
+
+```go
+package main
+
+import (
+ "log"
+ "net/http"
+
+ "github.com/doganarif/govisual"
+ "github.com/doganarif/govisual/internal/server"
+)
+
+func main() {
+ // Create a store for visualization data
+ store, err := govisual.NewStore(
+ govisual.WithMaxRequests(1000),
+ govisual.WithMemoryStorage(),
+ )
+ if err != nil {
+ log.Fatalf("Failed to create store: %v", err)
+ }
+
+ // Create a server mux
+ mux := http.NewServeMux()
+
+ // Add homepage
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ w.Write([]byte("
GoVisual Dashboard Go to /__viz to see the dashboard
"))
+ })
+
+ // Register agent API endpoints
+ agentAPI := server.NewAgentAPI(store)
+ agentAPI.RegisterHandlers(mux)
+
+ // Wrap with GoVisual
+ handler := govisual.Wrap(
+ mux,
+ govisual.WithMaxRequests(1000),
+ govisual.WithSharedStore(store),
+ )
+
+ // Start HTTP server
+ log.Println("Starting dashboard server on :8080")
+ if err := http.ListenAndServe(":8080", handler); err != nil {
+ log.Fatalf("HTTP server error: %v", err)
+ }
+}
+```
+
+For NATS transport, you also need to add a NATS handler:
+
+```go
+// Set up NATS handler if using NATS transport
+natsHandler, err := server.NewNATSHandler(store, "nats://localhost:4222")
+if err != nil {
+ log.Fatalf("Failed to create NATS handler: %v", err)
+}
+
+if err := natsHandler.Start(); err != nil {
+ log.Fatalf("Failed to start NATS handler: %v", err)
+}
+defer natsHandler.Stop()
+```
+
+### 2. Setting Up gRPC Agents
+
+Here's how to set up a gRPC agent with different transport options:
+
+#### Shared Store Transport (Local Services)
+
+```go
+package main
+
+import (
+ "log"
+ "net"
+
+ "github.com/doganarif/govisual"
+ "google.golang.org/grpc"
+ "your-service/proto"
+)
+
+func main() {
+ // Create or access shared store
+ sharedStore, err := govisual.NewStore(
+ govisual.WithMaxRequests(100),
+ govisual.WithMemoryStorage(),
+ )
+ if err != nil {
+ log.Fatalf("Failed to create store: %v", err)
+ }
+
+ // Create store transport
+ transport := govisual.NewStoreTransport(sharedStore)
+
+ // Create gRPC agent
+ agent := govisual.NewGRPCAgent(transport,
+ govisual.WithGRPCRequestDataLogging(true),
+ govisual.WithGRPCResponseDataLogging(true),
+ )
+
+ // Create gRPC server with agent
+ server := govisual.NewGRPCServer(agent)
+ proto.RegisterYourServiceServer(server, &YourServiceImpl{})
+
+ // Start server
+ lis, err := net.Listen("tcp", ":9000")
+ if err != nil {
+ log.Fatalf("Failed to listen: %v", err)
+ }
+ log.Println("Starting gRPC server on :9000")
+ if err := server.Serve(lis); err != nil {
+ log.Fatalf("Failed to serve: %v", err)
+ }
+}
+```
+
+#### HTTP Transport (Remote Services)
+
+```go
+// Create HTTP transport
+transport := govisual.NewHTTPTransport("http://dashboard-server:8080/api/agent/logs",
+ govisual.WithTimeout(5*time.Second),
+ govisual.WithMaxRetries(3),
+)
+
+// Create gRPC agent
+agent := govisual.NewGRPCAgent(transport,
+ govisual.WithGRPCRequestDataLogging(true),
+ govisual.WithGRPCResponseDataLogging(true),
+ govisual.WithBatchingEnabled(true),
+ govisual.WithBatchSize(10),
+ govisual.WithBatchInterval(3*time.Second),
+)
+```
+
+#### NATS Transport (Distributed Systems)
+
+```go
+// Create NATS transport
+transport, err := govisual.NewNATSTransport("nats://nats-server:4222",
+ govisual.WithMaxRetries(5),
+ govisual.WithCredentials(map[string]string{
+ "username": "user",
+ "password": "pass",
+ }),
+)
+if err != nil {
+ log.Fatalf("Failed to create NATS transport: %v", err)
+}
+
+// Create gRPC agent
+agent := govisual.NewGRPCAgent(transport,
+ govisual.WithGRPCRequestDataLogging(true),
+ govisual.WithGRPCResponseDataLogging(true),
+)
+```
+
+### 3. Setting Up HTTP Agents
+
+For HTTP services, use the HTTP agent:
+
+```go
+// Create transport
+transport := govisual.NewHTTPTransport("http://dashboard-server:8080/api/agent/logs")
+
+// Create HTTP agent
+agent := govisual.NewHTTPAgent(transport,
+ govisual.WithHTTPRequestBodyLogging(true),
+ govisual.WithHTTPResponseBodyLogging(true),
+ govisual.WithMaxBodySize(1024*1024), // 1MB
+ govisual.WithIgnorePaths("/health", "/metrics"),
+ govisual.WithIgnoreExtensions(".jpg", ".png", ".css"),
+)
+
+// Apply as middleware to your HTTP server
+mux := http.NewServeMux()
+mux.HandleFunc("/", yourHandler)
+
+// Wrap with agent middleware
+http.ListenAndServe(":8000", agent.Middleware(mux))
+```
+
+## Configuration Options
+
+### Agent Options
+
+#### Common Options
+
+```go
+// Set maximum buffer size for when transport is unavailable
+govisual.WithMaxBufferSize(100)
+
+// Enable batching to reduce transport overhead
+govisual.WithBatchingEnabled(true)
+
+// Set batch size
+govisual.WithBatchSize(20)
+
+// Set batch interval
+govisual.WithBatchInterval(5*time.Second)
+
+// Add filtering to exclude certain requests
+govisual.WithFilter(func(log *model.RequestLog) bool {
+ // Skip health check endpoints
+ if log.Type == model.TypeHTTP && log.Path == "/health" {
+ return false
+ }
+ return true
+})
+
+// Add processing to modify or clean up logs before transport
+govisual.WithProcessor(func(log *model.RequestLog) *model.RequestLog {
+ // Redact sensitive information
+ if log.Type == model.TypeHTTP && strings.Contains(log.Path, "/auth") {
+ log.RequestBody = "[REDACTED]"
+ }
+ return log
+})
+```
+
+#### gRPC Agent Options
+
+```go
+// Log request message data
+govisual.WithGRPCRequestDataLogging(true)
+
+// Log response message data
+govisual.WithGRPCResponseDataLogging(true)
+
+// Ignore specific gRPC methods
+govisual.WithIgnoreGRPCMethods(
+ "/health.HealthService/Check",
+ "/grpc.reflection.v1.ReflectionService/*",
+)
+```
+
+#### HTTP Agent Options
+
+```go
+// Log request bodies
+govisual.WithHTTPRequestBodyLogging(true)
+
+// Log response bodies
+govisual.WithHTTPResponseBodyLogging(true)
+
+// Set maximum body size to log
+govisual.WithMaxBodySize(512*1024) // 512KB
+
+// Ignore specific paths
+govisual.WithIgnorePaths("/health", "/metrics", "/favicon.ico")
+
+// Ignore specific file extensions
+govisual.WithIgnoreExtensions(".jpg", ".png", ".gif", ".css", ".js")
+
+// Transform paths before logging (e.g., to normalize UUIDs)
+govisual.WithPathTransformer(func(path string) string {
+ // Replace UUIDs with placeholders
+ return regexp.MustCompile(`/users/[0-9a-f-]{36}`).
+ ReplaceAllString(path, "/users/:id")
+})
+```
+
+### Transport Options
+
+```go
+// Set endpoint for HTTP transport
+govisual.WithEndpoint("http://dashboard-server:8080/api/agent/logs")
+
+// Add authentication credentials
+govisual.WithCredentials(map[string]string{
+ "token": "your-auth-token",
+ "api_key": "your-api-key",
+})
+
+// Configure retries
+govisual.WithMaxRetries(5)
+govisual.WithRetryWait(2*time.Second)
+
+// Set timeout
+govisual.WithTimeout(10*time.Second)
+
+// Set buffer size for when transport is unavailable
+govisual.WithBufferSize(200)
+```
+
+## Deployment Scenarios
+
+### Single Service (Development)
+
+For local development with a single service, use the shared store transport:
+
+```
+┌──────────────────┐
+│ Single Service │
+│ │
+│ ┌─────────────┐ │
+│ │ gRPC/HTTP │ │
+│ │ Agent │ │
+│ └─────┬───────┘ │
+│ │ │
+│ ┌─────▼───────┐ │
+│ │ Shared │ │
+│ │ Store │ │
+│ └─────┬───────┘ │
+│ │ │
+│ ┌─────▼───────┐ │
+│ │ Dashboard │ │
+│ └─────────────┘ │
+└──────────────────┘
+```
+
+### Multiple Services (Single Machine)
+
+For multiple services on a single machine, use the shared store transport:
+
+```
+┌──────────────────┐ ┌──────────────────┐
+│ Service A (gRPC)│ │ Service B (HTTP)│
+│ │ │ │
+│ ┌─────────────┐ │ │ ┌─────────────┐ │
+│ │ gRPC Agent │ │ │ │ HTTP Agent │ │
+│ └──────┬──────┘ │ │ └──────┬──────┘ │
+└────────┼─────────┘ └────────┼─────────┘
+ │ │
+ ▼ ▼
+┌───────────────────────────────────────┐
+│ Shared Store │
+└─────────────────┬─────────────────────┘
+ │
+┌─────────────────▼─────────────────────┐
+│ Dashboard Server │
+└───────────────────────────────────────┘
+```
+
+### Distributed Services (Multiple Machines)
+
+For distributed services, use the HTTP or NATS transport:
+
+```
+┌──────────────────┐ ┌──────────────────┐
+│ Service A (gRPC)│ │ Service B (HTTP)│
+│ │ │ │
+│ ┌─────────────┐ │ │ ┌─────────────┐ │
+│ │ gRPC Agent │ │ │ │ HTTP Agent │ │
+│ └──────┬──────┘ │ │ └──────┬──────┘ │
+└────────┼─────────┘ └────────┼─────────┘
+ │ │
+ │ │
+ ▼ ▼
+┌───────────────────────────────────────┐
+│ Transport Layer │
+│ (NATS or HTTP Transport) │
+└─────────────────┬─────────────────────┘
+ │
+ │
+┌─────────────────▼─────────────────────┐
+│ Dashboard Server │
+│ │
+│ ┌────────────────────────────────────┐ │
+│ │ Store Backend │ │
+│ └────────────────────────────────────┘ │
+└───────────────────────────────────────┘
+```
+
+## Best Practices
+
+### Security Considerations
+
+1. **Data Privacy**: Use the processor option to redact sensitive information from logs before transport:
+
+```go
+govisual.WithProcessor(func(log *model.RequestLog) *model.RequestLog {
+ // Redact authentication tokens
+ if log.Type == model.TypeHTTP {
+ // Redact Authorization headers
+ if auth, ok := log.RequestHeaders["Authorization"]; ok {
+ log.RequestHeaders["Authorization"] = []string{"[REDACTED]"}
+ }
+
+ // Redact sensitive JSON fields in bodies
+ if strings.Contains(log.Path, "/users") || strings.Contains(log.Path, "/accounts") {
+ // Use a JSON parser to selectively redact fields
+ if strings.Contains(log.RequestBody, "password") ||
+ strings.Contains(log.RequestBody, "credit_card") {
+ // Replace with redacted version
+ log.RequestBody = redactSensitiveJSON(log.RequestBody)
+ }
+ }
+ }
+ return log
+})
+```
+
+2. **Transport Security**: Use secure connections for remote transports:
+
+```go
+// For HTTP transport
+transport := govisual.NewHTTPTransport("https://dashboard-server:8080/api/agent/logs",
+ govisual.WithCredentials(map[string]string{
+ "token": "your-secure-token",
+ }),
+)
+
+// For NATS transport with TLS
+transport, err := govisual.NewNATSTransport("nats://nats-server:4222",
+ govisual.WithCredentials(map[string]string{
+ "token": "your-nats-token",
+ }),
+ // Add TLS configurations
+)
+```
+
+### Performance Considerations
+
+1. **Batching**: Enable batching to reduce network overhead for remote transports:
+
+```go
+agent := govisual.NewGRPCAgent(transport,
+ govisual.WithBatchingEnabled(true),
+ govisual.WithBatchSize(20),
+ govisual.WithBatchInterval(5*time.Second),
+)
+```
+
+2. **Filtering**: Filter out high-volume, low-value requests to reduce load:
+
+```go
+govisual.WithFilter(func(log *model.RequestLog) bool {
+ // Skip static assets
+ if log.Type == model.TypeHTTP {
+ if strings.HasPrefix(log.Path, "/static/") ||
+ strings.HasPrefix(log.Path, "/assets/") {
+ return false
+ }
+ }
+
+ // Skip health checks and metrics endpoints
+ if log.Type == model.TypeGRPC &&
+ (strings.Contains(log.GRPCService, "Health") ||
+ strings.Contains(log.GRPCService, "Metrics")) {
+ return false
+ }
+
+ return true
+})
+```
+
+3. **Body Size Limits**: Limit the size of request/response bodies to prevent memory issues:
+
+```go
+// For HTTP agent
+govisual.WithMaxBodySize(512*1024) // 512KB limit
+
+// For gRPC agent, process large messages
+govisual.WithProcessor(func(log *model.RequestLog) *model.RequestLog {
+ // Truncate large request/response data
+ const maxSize = 1024 * 1024 // 1MB
+
+ if log.Type == model.TypeGRPC {
+ if len(log.GRPCRequestData) > maxSize {
+ log.GRPCRequestData = log.GRPCRequestData[:maxSize] + "... [TRUNCATED]"
+ }
+
+ if len(log.GRPCResponseData) > maxSize {
+ log.GRPCResponseData = log.GRPCResponseData[:maxSize] + "... [TRUNCATED]"
+ }
+ }
+
+ return log
+})
+```
+
+### Monitoring & Troubleshooting
+
+Add logging to help debug agent and transport issues:
+
+```go
+// Create a processor that logs when errors occur
+govisual.WithProcessor(func(log *model.RequestLog) *model.RequestLog {
+ if log.Error != "" {
+ internalLogger.Debugf("Request error captured: %s, path: %s", log.Error, log.Path)
+ }
+
+ // For gRPC status codes other than OK
+ if log.Type == model.TypeGRPC && log.GRPCStatusCode != 0 {
+ internalLogger.Debugf("gRPC request failed: status=%d, desc=%s, method=%s/%s",
+ log.GRPCStatusCode, log.GRPCStatusDesc, log.GRPCService, log.GRPCMethod)
+ }
+
+ return log
+})
+```
+
+## Advanced Use Cases
+
+### Custom Transport Implementation
+
+You can implement your own transport by implementing the `transport.Transport` interface:
+
+```go
+type CustomTransport struct {
+ // Custom fields
+}
+
+func NewCustomTransport() *CustomTransport {
+ return &CustomTransport{}
+}
+
+func (t *CustomTransport) Send(log *model.RequestLog) error {
+ // Implement your custom sending logic
+ return nil
+}
+
+func (t *CustomTransport) SendBatch(logs []*model.RequestLog) error {
+ // Implement your custom batch sending logic
+ return nil
+}
+
+func (t *CustomTransport) Close() error {
+ // Clean up resources
+ return nil
+}
+```
+
+### Multiple Transport Targets
+
+To send data to multiple visualization servers, create a composite transport:
+
+```go
+type CompositeTransport struct {
+ transports []transport.Transport
+}
+
+func NewCompositeTransport(transports ...transport.Transport) *CompositeTransport {
+ return &CompositeTransport{
+ transports: transports,
+ }
+}
+
+func (t *CompositeTransport) Send(log *model.RequestLog) error {
+ var lastErr error
+ for _, transport := range t.transports {
+ if err := transport.Send(log); err != nil {
+ lastErr = err
+ }
+ }
+ return lastErr
+}
+
+func (t *CompositeTransport) SendBatch(logs []*model.RequestLog) error {
+ var lastErr error
+ for _, transport := range t.transports {
+ if err := transport.SendBatch(logs); err != nil {
+ lastErr = err
+ }
+ }
+ return lastErr
+}
+
+func (t *CompositeTransport) Close() error {
+ var lastErr error
+ for _, transport := range t.transports {
+ if err := transport.Close(); err != nil {
+ lastErr = err
+ }
+ }
+ return lastErr
+}
+
+// Usage
+httpTransport := govisual.NewHTTPTransport("http://dashboard-1:8080/api/agent/logs")
+natsTransport, _ := govisual.NewNATSTransport("nats://nats-server:4222")
+compositeTransport := NewCompositeTransport(httpTransport, natsTransport)
+
+agent := govisual.NewGRPCAgent(compositeTransport)
+```
+
+## Compatibility with Existing Code
+
+The agent architecture is designed to be backward compatible with the existing GoVisual API. You can gradually migrate your codebase to use agents:
+
+### Before (Direct Dashboard)
+
+```go
+grpcServer := grpc.NewServer(
+ govisual.WrapGRPCServer(
+ govisual.WithGRPC(true),
+ govisual.WithGRPCRequestDataLogging(true),
+ govisual.WithGRPCResponseDataLogging(true),
+ )...)
+```
+
+### After (Agent Architecture)
+
+```go
+// Create transport
+transport := govisual.NewStoreTransport(sharedStore)
+
+// Create agent
+agent := govisual.NewGRPCAgent(transport,
+ govisual.WithGRPCRequestDataLogging(true),
+ govisual.WithGRPCResponseDataLogging(true),
+)
+
+// Create server with agent
+grpcServer := govisual.NewGRPCServer(agent)
+```
+
+## Conclusion
+
+The agent architecture provides a flexible way to collect and visualize request data from distributed services. By separating data collection (agents) from visualization (dashboard), GoVisual can now monitor complex, multi-service architectures while providing options for different deployment scenarios.
+
+Choose the right transport mechanism based on your deployment needs:
+
+- **Shared Store**: For services running on the same machine
+- **HTTP Transport**: For remote services with direct HTTP connectivity
+- **NATS Transport**: For distributed systems where a message broker is available
+
+The architecture is designed to be extensible, allowing for custom transport implementations and advanced use cases.
diff --git a/cmd/examples/grpc_agent/amalgo.txt b/cmd/examples/grpc_agent/amalgo.txt
new file mode 100644
index 0000000..fe0bab4
--- /dev/null
+++ b/cmd/examples/grpc_agent/amalgo.txt
@@ -0,0 +1,1579 @@
+## Generated with Amalgo at: 2025-05-14 20:41:24
+
+## File Tree
+
+└── grpc_agent/
+ ├── README.md
+ ├── grpc_server.go
+ ├── main.go
+ └── gen/
+ └── greeter/
+ └── v1/
+ ├── greeter.pb.go
+ └── greeter_grpc.pb.go
+
+## File Contents
+
+--- Start File: grpc_agent/README.md
+# GoVisual Agent Architecture
+
+This document explains how to use the new agent architecture in GoVisual to monitor distributed services.
+
+## What is the Agent Architecture?
+
+The agent architecture allows GoVisual to collect request data from multiple services (potentially running on different machines) and visualize them in a central dashboard. This is particularly useful for microservice architectures or distributed systems where multiple services need to be monitored.
+
+### Key Components
+
+1. **Agents**: Lightweight components that attach to services (gRPC, HTTP) to collect request/response data
+2. **Transports**: Mechanisms for sending data from agents to the visualization server
+3. **Visualization Server**: Central server that receives, stores, and displays the request data
+
+## Getting Started
+
+### 1. Setting Up the Visualization Server
+
+Start by initializing a visualization server that will display the dashboard and receive agent data:
+
+```go
+package main
+
+import (
+ "log"
+ "net/http"
+
+ "github.com/doganarif/govisual"
+ "github.com/doganarif/govisual/internal/server"
+)
+
+func main() {
+ // Create a store for visualization data
+ store, err := govisual.NewStore(
+ govisual.WithMaxRequests(1000),
+ govisual.WithMemoryStorage(),
+ )
+ if err != nil {
+ log.Fatalf("Failed to create store: %v", err)
+ }
+
+ // Create a server mux
+ mux := http.NewServeMux()
+
+ // Add homepage
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ w.Write([]byte("GoVisual Dashboard Go to /__viz to see the dashboard
"))
+ })
+
+ // Register agent API endpoints
+ agentAPI := server.NewAgentAPI(store)
+ agentAPI.RegisterHandlers(mux)
+
+ // Wrap with GoVisual
+ handler := govisual.Wrap(
+ mux,
+ govisual.WithMaxRequests(1000),
+ govisual.WithSharedStore(store),
+ )
+
+ // Start HTTP server
+ log.Println("Starting dashboard server on :8080")
+ if err := http.ListenAndServe(":8080", handler); err != nil {
+ log.Fatalf("HTTP server error: %v", err)
+ }
+}
+```
+
+For NATS transport, you also need to add a NATS handler:
+
+```go
+// Set up NATS handler if using NATS transport
+natsHandler, err := server.NewNATSHandler(store, "nats://localhost:4222")
+if err != nil {
+ log.Fatalf("Failed to create NATS handler: %v", err)
+}
+
+if err := natsHandler.Start(); err != nil {
+ log.Fatalf("Failed to start NATS handler: %v", err)
+}
+defer natsHandler.Stop()
+```
+
+### 2. Setting Up gRPC Agents
+
+Here's how to set up a gRPC agent with different transport options:
+
+#### Shared Store Transport (Local Services)
+
+```go
+package main
+
+import (
+ "log"
+ "net"
+
+ "github.com/doganarif/govisual"
+ "google.golang.org/grpc"
+ "your-service/proto"
+)
+
+func main() {
+ // Create or access shared store
+ sharedStore, err := govisual.NewStore(
+ govisual.WithMaxRequests(100),
+ govisual.WithMemoryStorage(),
+ )
+ if err != nil {
+ log.Fatalf("Failed to create store: %v", err)
+ }
+
+ // Create store transport
+ transport := govisual.NewStoreTransport(sharedStore)
+
+ // Create gRPC agent
+ agent := govisual.NewGRPCAgent(transport,
+ govisual.WithGRPCRequestDataLogging(true),
+ govisual.WithGRPCResponseDataLogging(true),
+ )
+
+ // Create gRPC server with agent
+ server := govisual.NewGRPCServer(agent)
+ proto.RegisterYourServiceServer(server, &YourServiceImpl{})
+
+ // Start server
+ lis, err := net.Listen("tcp", ":9000")
+ if err != nil {
+ log.Fatalf("Failed to listen: %v", err)
+ }
+ log.Println("Starting gRPC server on :9000")
+ if err := server.Serve(lis); err != nil {
+ log.Fatalf("Failed to serve: %v", err)
+ }
+}
+```
+
+#### HTTP Transport (Remote Services)
+
+```go
+// Create HTTP transport
+transport := govisual.NewHTTPTransport("http://dashboard-server:8080/api/agent/logs",
+ govisual.WithTimeout(5*time.Second),
+ govisual.WithMaxRetries(3),
+)
+
+// Create gRPC agent
+agent := govisual.NewGRPCAgent(transport,
+ govisual.WithGRPCRequestDataLogging(true),
+ govisual.WithGRPCResponseDataLogging(true),
+ govisual.WithBatchingEnabled(true),
+ govisual.WithBatchSize(10),
+ govisual.WithBatchInterval(3*time.Second),
+)
+```
+
+#### NATS Transport (Distributed Systems)
+
+```go
+// Create NATS transport
+transport, err := govisual.NewNATSTransport("nats://nats-server:4222",
+ govisual.WithMaxRetries(5),
+ govisual.WithCredentials(map[string]string{
+ "username": "user",
+ "password": "pass",
+ }),
+)
+if err != nil {
+ log.Fatalf("Failed to create NATS transport: %v", err)
+}
+
+// Create gRPC agent
+agent := govisual.NewGRPCAgent(transport,
+ govisual.WithGRPCRequestDataLogging(true),
+ govisual.WithGRPCResponseDataLogging(true),
+)
+```
+
+### 3. Setting Up HTTP Agents
+
+For HTTP services, use the HTTP agent:
+
+```go
+// Create transport
+transport := govisual.NewHTTPTransport("http://dashboard-server:8080/api/agent/logs")
+
+// Create HTTP agent
+agent := govisual.NewHTTPAgent(transport,
+ govisual.WithHTTPRequestBodyLogging(true),
+ govisual.WithHTTPResponseBodyLogging(true),
+ govisual.WithMaxBodySize(1024*1024), // 1MB
+ govisual.WithIgnorePaths("/health", "/metrics"),
+ govisual.WithIgnoreExtensions(".jpg", ".png", ".css"),
+)
+
+// Apply as middleware to your HTTP server
+mux := http.NewServeMux()
+mux.HandleFunc("/", yourHandler)
+
+// Wrap with agent middleware
+http.ListenAndServe(":8000", agent.Middleware(mux))
+```
+
+## Configuration Options
+
+### Agent Options
+
+#### Common Options
+
+```go
+// Set maximum buffer size for when transport is unavailable
+govisual.WithMaxBufferSize(100)
+
+// Enable batching to reduce transport overhead
+govisual.WithBatchingEnabled(true)
+
+// Set batch size
+govisual.WithBatchSize(20)
+
+// Set batch interval
+govisual.WithBatchInterval(5*time.Second)
+
+// Add filtering to exclude certain requests
+govisual.WithFilter(func(log *model.RequestLog) bool {
+ // Skip health check endpoints
+ if log.Type == model.TypeHTTP && log.Path == "/health" {
+ return false
+ }
+ return true
+})
+
+// Add processing to modify or clean up logs before transport
+govisual.WithProcessor(func(log *model.RequestLog) *model.RequestLog {
+ // Redact sensitive information
+ if log.Type == model.TypeHTTP && strings.Contains(log.Path, "/auth") {
+ log.RequestBody = "[REDACTED]"
+ }
+ return log
+})
+```
+
+#### gRPC Agent Options
+
+```go
+// Log request message data
+govisual.WithGRPCRequestDataLogging(true)
+
+// Log response message data
+govisual.WithGRPCResponseDataLogging(true)
+
+// Ignore specific gRPC methods
+govisual.WithIgnoreGRPCMethods(
+ "/health.HealthService/Check",
+ "/grpc.reflection.v1.ReflectionService/*",
+)
+```
+
+#### HTTP Agent Options
+
+```go
+// Log request bodies
+govisual.WithHTTPRequestBodyLogging(true)
+
+// Log response bodies
+govisual.WithHTTPResponseBodyLogging(true)
+
+// Set maximum body size to log
+govisual.WithMaxBodySize(512*1024) // 512KB
+
+// Ignore specific paths
+govisual.WithIgnorePaths("/health", "/metrics", "/favicon.ico")
+
+// Ignore specific file extensions
+govisual.WithIgnoreExtensions(".jpg", ".png", ".gif", ".css", ".js")
+
+// Transform paths before logging (e.g., to normalize UUIDs)
+govisual.WithPathTransformer(func(path string) string {
+ // Replace UUIDs with placeholders
+ return regexp.MustCompile(`/users/[0-9a-f-]{36}`).
+ ReplaceAllString(path, "/users/:id")
+})
+```
+
+### Transport Options
+
+```go
+// Set endpoint for HTTP transport
+govisual.WithEndpoint("http://dashboard-server:8080/api/agent/logs")
+
+// Add authentication credentials
+govisual.WithCredentials(map[string]string{
+ "token": "your-auth-token",
+ "api_key": "your-api-key",
+})
+
+// Configure retries
+govisual.WithMaxRetries(5)
+govisual.WithRetryWait(2*time.Second)
+
+// Set timeout
+govisual.WithTimeout(10*time.Second)
+
+// Set buffer size for when transport is unavailable
+govisual.WithBufferSize(200)
+```
+
+## Deployment Scenarios
+
+### Single Service (Development)
+
+For local development with a single service, use the shared store transport:
+
+```
+┌──────────────────┐
+│ Single Service │
+│ │
+│ ┌─────────────┐ │
+│ │ gRPC/HTTP │ │
+│ │ Agent │ │
+│ └─────┬───────┘ │
+│ │ │
+│ ┌─────▼───────┐ │
+│ │ Shared │ │
+│ │ Store │ │
+│ └─────┬───────┘ │
+│ │ │
+│ ┌─────▼───────┐ │
+│ │ Dashboard │ │
+│ └─────────────┘ │
+└──────────────────┘
+```
+
+### Multiple Services (Single Machine)
+
+For multiple services on a single machine, use the shared store transport:
+
+```
+┌──────────────────┐ ┌──────────────────┐
+│ Service A (gRPC)│ │ Service B (HTTP)│
+│ │ │ │
+│ ┌─────────────┐ │ │ ┌─────────────┐ │
+│ │ gRPC Agent │ │ │ │ HTTP Agent │ │
+│ └──────┬──────┘ │ │ └──────┬──────┘ │
+└────────┼─────────┘ └────────┼─────────┘
+ │ │
+ ▼ ▼
+┌───────────────────────────────────────┐
+│ Shared Store │
+└─────────────────┬─────────────────────┘
+ │
+┌─────────────────▼─────────────────────┐
+│ Dashboard Server │
+└───────────────────────────────────────┘
+```
+
+### Distributed Services (Multiple Machines)
+
+For distributed services, use the HTTP or NATS transport:
+
+```
+┌──────────────────┐ ┌──────────────────┐
+│ Service A (gRPC)│ │ Service B (HTTP)│
+│ │ │ │
+│ ┌─────────────┐ │ │ ┌─────────────┐ │
+│ │ gRPC Agent │ │ │ │ HTTP Agent │ │
+│ └──────┬──────┘ │ │ └──────┬──────┘ │
+└────────┼─────────┘ └────────┼─────────┘
+ │ │
+ │ │
+ ▼ ▼
+┌───────────────────────────────────────┐
+│ Transport Layer │
+│ (NATS or HTTP Transport) │
+└─────────────────┬─────────────────────┘
+ │
+ │
+┌─────────────────▼─────────────────────┐
+│ Dashboard Server │
+│ │
+│ ┌────────────────────────────────────┐ │
+│ │ Store Backend │ │
+│ └────────────────────────────────────┘ │
+└───────────────────────────────────────┘
+```
+
+## Best Practices
+
+### Security Considerations
+
+1. **Data Privacy**: Use the processor option to redact sensitive information from logs before transport:
+
+```go
+govisual.WithProcessor(func(log *model.RequestLog) *model.RequestLog {
+ // Redact authentication tokens
+ if log.Type == model.TypeHTTP {
+ // Redact Authorization headers
+ if auth, ok := log.RequestHeaders["Authorization"]; ok {
+ log.RequestHeaders["Authorization"] = []string{"[REDACTED]"}
+ }
+
+ // Redact sensitive JSON fields in bodies
+ if strings.Contains(log.Path, "/users") || strings.Contains(log.Path, "/accounts") {
+ // Use a JSON parser to selectively redact fields
+ if strings.Contains(log.RequestBody, "password") ||
+ strings.Contains(log.RequestBody, "credit_card") {
+ // Replace with redacted version
+ log.RequestBody = redactSensitiveJSON(log.RequestBody)
+ }
+ }
+ }
+ return log
+})
+```
+
+2. **Transport Security**: Use secure connections for remote transports:
+
+```go
+// For HTTP transport
+transport := govisual.NewHTTPTransport("https://dashboard-server:8080/api/agent/logs",
+ govisual.WithCredentials(map[string]string{
+ "token": "your-secure-token",
+ }),
+)
+
+// For NATS transport with TLS
+transport, err := govisual.NewNATSTransport("nats://nats-server:4222",
+ govisual.WithCredentials(map[string]string{
+ "token": "your-nats-token",
+ }),
+ // Add TLS configurations
+)
+```
+
+### Performance Considerations
+
+1. **Batching**: Enable batching to reduce network overhead for remote transports:
+
+```go
+agent := govisual.NewGRPCAgent(transport,
+ govisual.WithBatchingEnabled(true),
+ govisual.WithBatchSize(20),
+ govisual.WithBatchInterval(5*time.Second),
+)
+```
+
+2. **Filtering**: Filter out high-volume, low-value requests to reduce load:
+
+```go
+govisual.WithFilter(func(log *model.RequestLog) bool {
+ // Skip static assets
+ if log.Type == model.TypeHTTP {
+ if strings.HasPrefix(log.Path, "/static/") ||
+ strings.HasPrefix(log.Path, "/assets/") {
+ return false
+ }
+ }
+
+ // Skip health checks and metrics endpoints
+ if log.Type == model.TypeGRPC &&
+ (strings.Contains(log.GRPCService, "Health") ||
+ strings.Contains(log.GRPCService, "Metrics")) {
+ return false
+ }
+
+ return true
+})
+```
+
+3. **Body Size Limits**: Limit the size of request/response bodies to prevent memory issues:
+
+```go
+// For HTTP agent
+govisual.WithMaxBodySize(512*1024) // 512KB limit
+
+// For gRPC agent, process large messages
+govisual.WithProcessor(func(log *model.RequestLog) *model.RequestLog {
+ // Truncate large request/response data
+ const maxSize = 1024 * 1024 // 1MB
+
+ if log.Type == model.TypeGRPC {
+ if len(log.GRPCRequestData) > maxSize {
+ log.GRPCRequestData = log.GRPCRequestData[:maxSize] + "... [TRUNCATED]"
+ }
+
+ if len(log.GRPCResponseData) > maxSize {
+ log.GRPCResponseData = log.GRPCResponseData[:maxSize] + "... [TRUNCATED]"
+ }
+ }
+
+ return log
+})
+```
+
+### Monitoring & Troubleshooting
+
+Add logging to help debug agent and transport issues:
+
+```go
+// Create a processor that logs when errors occur
+govisual.WithProcessor(func(log *model.RequestLog) *model.RequestLog {
+ if log.Error != "" {
+ internalLogger.Debugf("Request error captured: %s, path: %s", log.Error, log.Path)
+ }
+
+ // For gRPC status codes other than OK
+ if log.Type == model.TypeGRPC && log.GRPCStatusCode != 0 {
+ internalLogger.Debugf("gRPC request failed: status=%d, desc=%s, method=%s/%s",
+ log.GRPCStatusCode, log.GRPCStatusDesc, log.GRPCService, log.GRPCMethod)
+ }
+
+ return log
+})
+```
+
+## Advanced Use Cases
+
+### Custom Transport Implementation
+
+You can implement your own transport by implementing the `transport.Transport` interface:
+
+```go
+type CustomTransport struct {
+ // Custom fields
+}
+
+func NewCustomTransport() *CustomTransport {
+ return &CustomTransport{}
+}
+
+func (t *CustomTransport) Send(log *model.RequestLog) error {
+ // Implement your custom sending logic
+ return nil
+}
+
+func (t *CustomTransport) SendBatch(logs []*model.RequestLog) error {
+ // Implement your custom batch sending logic
+ return nil
+}
+
+func (t *CustomTransport) Close() error {
+ // Clean up resources
+ return nil
+}
+```
+
+### Multiple Transport Targets
+
+To send data to multiple visualization servers, create a composite transport:
+
+```go
+type CompositeTransport struct {
+ transports []transport.Transport
+}
+
+func NewCompositeTransport(transports ...transport.Transport) *CompositeTransport {
+ return &CompositeTransport{
+ transports: transports,
+ }
+}
+
+func (t *CompositeTransport) Send(log *model.RequestLog) error {
+ var lastErr error
+ for _, transport := range t.transports {
+ if err := transport.Send(log); err != nil {
+ lastErr = err
+ }
+ }
+ return lastErr
+}
+
+func (t *CompositeTransport) SendBatch(logs []*model.RequestLog) error {
+ var lastErr error
+ for _, transport := range t.transports {
+ if err := transport.SendBatch(logs); err != nil {
+ lastErr = err
+ }
+ }
+ return lastErr
+}
+
+func (t *CompositeTransport) Close() error {
+ var lastErr error
+ for _, transport := range t.transports {
+ if err := transport.Close(); err != nil {
+ lastErr = err
+ }
+ }
+ return lastErr
+}
+
+// Usage
+httpTransport := govisual.NewHTTPTransport("http://dashboard-1:8080/api/agent/logs")
+natsTransport, _ := govisual.NewNATSTransport("nats://nats-server:4222")
+compositeTransport := NewCompositeTransport(httpTransport, natsTransport)
+
+agent := govisual.NewGRPCAgent(compositeTransport)
+```
+
+## Compatibility with Existing Code
+
+The agent architecture is designed to be backward compatible with the existing GoVisual API. You can gradually migrate your codebase to use agents:
+
+### Before (Direct Dashboard)
+
+```go
+grpcServer := grpc.NewServer(
+ govisual.WrapGRPCServer(
+ govisual.WithGRPC(true),
+ govisual.WithGRPCRequestDataLogging(true),
+ govisual.WithGRPCResponseDataLogging(true),
+ )...)
+```
+
+### After (Agent Architecture)
+
+```go
+// Create transport
+transport := govisual.NewStoreTransport(sharedStore)
+
+// Create agent
+agent := govisual.NewGRPCAgent(transport,
+ govisual.WithGRPCRequestDataLogging(true),
+ govisual.WithGRPCResponseDataLogging(true),
+)
+
+// Create server with agent
+grpcServer := govisual.NewGRPCServer(agent)
+```
+
+## Conclusion
+
+The agent architecture provides a flexible way to collect and visualize request data from distributed services. By separating data collection (agents) from visualization (dashboard), GoVisual can now monitor complex, multi-service architectures while providing options for different deployment scenarios.
+
+Choose the right transport mechanism based on your deployment needs:
+
+- **Shared Store**: For services running on the same machine
+- **HTTP Transport**: For remote services with direct HTTP connectivity
+- **NATS Transport**: For distributed systems where a message broker is available
+
+The architecture is designed to be extensible, allowing for custom transport implementations and advanced use cases.
+
+--- End File: grpc_agent/README.md
+
+--- Start File: grpc_agent/gen/greeter/v1/greeter.pb.go
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.36.6
+// protoc (unknown)
+// source: greeter/v1/greeter.proto
+
+package greeterv1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+ unsafe "unsafe"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// The request message containing the user's name.
+type HelloRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+ Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *HelloRequest) Reset() {
+ *x = HelloRequest{}
+ mi := &file_greeter_v1_greeter_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *HelloRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*HelloRequest) ProtoMessage() {}
+
+func (x *HelloRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_greeter_v1_greeter_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead.
+func (*HelloRequest) Descriptor() ([]byte, []int) {
+ return file_greeter_v1_greeter_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *HelloRequest) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *HelloRequest) GetMessage() string {
+ if x != nil {
+ return x.Message
+ }
+ return ""
+}
+
+// The response message containing the greetings.
+type HelloReply struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
+ Timestamp int64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *HelloReply) Reset() {
+ *x = HelloReply{}
+ mi := &file_greeter_v1_greeter_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *HelloReply) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*HelloReply) ProtoMessage() {}
+
+func (x *HelloReply) ProtoReflect() protoreflect.Message {
+ mi := &file_greeter_v1_greeter_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use HelloReply.ProtoReflect.Descriptor instead.
+func (*HelloReply) Descriptor() ([]byte, []int) {
+ return file_greeter_v1_greeter_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *HelloReply) GetMessage() string {
+ if x != nil {
+ return x.Message
+ }
+ return ""
+}
+
+func (x *HelloReply) GetTimestamp() int64 {
+ if x != nil {
+ return x.Timestamp
+ }
+ return 0
+}
+
+var File_greeter_v1_greeter_proto protoreflect.FileDescriptor
+
+const file_greeter_v1_greeter_proto_rawDesc = "" +
+ "\n" +
+ "\x18greeter/v1/greeter.proto\x12\n" +
+ "greeter.v1\"<\n" +
+ "\fHelloRequest\x12\x12\n" +
+ "\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" +
+ "\amessage\x18\x02 \x01(\tR\amessage\"D\n" +
+ "\n" +
+ "HelloReply\x12\x18\n" +
+ "\amessage\x18\x01 \x01(\tR\amessage\x12\x1c\n" +
+ "\ttimestamp\x18\x02 \x01(\x03R\ttimestamp2\xa4\x02\n" +
+ "\x0eGreeterService\x12>\n" +
+ "\bSayHello\x12\x18.greeter.v1.HelloRequest\x1a\x16.greeter.v1.HelloReply\"\x00\x12F\n" +
+ "\x0eSayHelloStream\x12\x18.greeter.v1.HelloRequest\x1a\x16.greeter.v1.HelloReply\"\x000\x01\x12E\n" +
+ "\rCollectHellos\x12\x18.greeter.v1.HelloRequest\x1a\x16.greeter.v1.HelloReply\"\x00(\x01\x12C\n" +
+ "\tChatHello\x12\x18.greeter.v1.HelloRequest\x1a\x16.greeter.v1.HelloReply\"\x00(\x010\x01B\xad\x01\n" +
+ "\x0ecom.greeter.v1B\fGreeterProtoP\x01ZDgithub.com/doganarif/govisual/examples/grpc/gen/greeter/v1;greeterv1\xa2\x02\x03GXX\xaa\x02\n" +
+ "Greeter.V1\xca\x02\n" +
+ "Greeter\\V1\xe2\x02\x16Greeter\\V1\\GPBMetadata\xea\x02\vGreeter::V1b\x06proto3"
+
+var (
+ file_greeter_v1_greeter_proto_rawDescOnce sync.Once
+ file_greeter_v1_greeter_proto_rawDescData []byte
+)
+
+func file_greeter_v1_greeter_proto_rawDescGZIP() []byte {
+ file_greeter_v1_greeter_proto_rawDescOnce.Do(func() {
+ file_greeter_v1_greeter_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_greeter_v1_greeter_proto_rawDesc), len(file_greeter_v1_greeter_proto_rawDesc)))
+ })
+ return file_greeter_v1_greeter_proto_rawDescData
+}
+
+var file_greeter_v1_greeter_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_greeter_v1_greeter_proto_goTypes = []any{
+ (*HelloRequest)(nil), // 0: greeter.v1.HelloRequest
+ (*HelloReply)(nil), // 1: greeter.v1.HelloReply
+}
+var file_greeter_v1_greeter_proto_depIdxs = []int32{
+ 0, // 0: greeter.v1.GreeterService.SayHello:input_type -> greeter.v1.HelloRequest
+ 0, // 1: greeter.v1.GreeterService.SayHelloStream:input_type -> greeter.v1.HelloRequest
+ 0, // 2: greeter.v1.GreeterService.CollectHellos:input_type -> greeter.v1.HelloRequest
+ 0, // 3: greeter.v1.GreeterService.ChatHello:input_type -> greeter.v1.HelloRequest
+ 1, // 4: greeter.v1.GreeterService.SayHello:output_type -> greeter.v1.HelloReply
+ 1, // 5: greeter.v1.GreeterService.SayHelloStream:output_type -> greeter.v1.HelloReply
+ 1, // 6: greeter.v1.GreeterService.CollectHellos:output_type -> greeter.v1.HelloReply
+ 1, // 7: greeter.v1.GreeterService.ChatHello:output_type -> greeter.v1.HelloReply
+ 4, // [4:8] is the sub-list for method output_type
+ 0, // [0:4] is the sub-list for method input_type
+ 0, // [0:0] is the sub-list for extension type_name
+ 0, // [0:0] is the sub-list for extension extendee
+ 0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_greeter_v1_greeter_proto_init() }
+func file_greeter_v1_greeter_proto_init() {
+ if File_greeter_v1_greeter_proto != nil {
+ return
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_greeter_v1_greeter_proto_rawDesc), len(file_greeter_v1_greeter_proto_rawDesc)),
+ NumEnums: 0,
+ NumMessages: 2,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_greeter_v1_greeter_proto_goTypes,
+ DependencyIndexes: file_greeter_v1_greeter_proto_depIdxs,
+ MessageInfos: file_greeter_v1_greeter_proto_msgTypes,
+ }.Build()
+ File_greeter_v1_greeter_proto = out.File
+ file_greeter_v1_greeter_proto_goTypes = nil
+ file_greeter_v1_greeter_proto_depIdxs = nil
+}
+
+--- End File: grpc_agent/gen/greeter/v1/greeter.pb.go
+
+--- Start File: grpc_agent/gen/greeter/v1/greeter_grpc.pb.go
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.5.1
+// - protoc (unknown)
+// source: greeter/v1/greeter.proto
+
+package greeterv1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.64.0 or later.
+const _ = grpc.SupportPackageIsVersion9
+
+const (
+ GreeterService_SayHello_FullMethodName = "/greeter.v1.GreeterService/SayHello"
+ GreeterService_SayHelloStream_FullMethodName = "/greeter.v1.GreeterService/SayHelloStream"
+ GreeterService_CollectHellos_FullMethodName = "/greeter.v1.GreeterService/CollectHellos"
+ GreeterService_ChatHello_FullMethodName = "/greeter.v1.GreeterService/ChatHello"
+)
+
+// GreeterServiceClient is the client API for GreeterService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+//
+// The greeting service definition.
+type GreeterServiceClient interface {
+ // Sends a greeting (Unary RPC)
+ SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
+ // Sends multiple greetings in response to a single request (Server streaming RPC)
+ SayHelloStream(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[HelloReply], error)
+ // Collects multiple greetings and responds once (Client streaming RPC)
+ CollectHellos(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[HelloRequest, HelloReply], error)
+ // Chat with greetings back and forth (Bidirectional streaming RPC)
+ ChatHello(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[HelloRequest, HelloReply], error)
+}
+
+type greeterServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewGreeterServiceClient(cc grpc.ClientConnInterface) GreeterServiceClient {
+ return &greeterServiceClient{cc}
+}
+
+func (c *greeterServiceClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(HelloReply)
+ err := c.cc.Invoke(ctx, GreeterService_SayHello_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *greeterServiceClient) SayHelloStream(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[HelloReply], error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ stream, err := c.cc.NewStream(ctx, &GreeterService_ServiceDesc.Streams[0], GreeterService_SayHelloStream_FullMethodName, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ x := &grpc.GenericClientStream[HelloRequest, HelloReply]{ClientStream: stream}
+ if err := x.ClientStream.SendMsg(in); err != nil {
+ return nil, err
+ }
+ if err := x.ClientStream.CloseSend(); err != nil {
+ return nil, err
+ }
+ return x, nil
+}
+
+// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
+type GreeterService_SayHelloStreamClient = grpc.ServerStreamingClient[HelloReply]
+
+func (c *greeterServiceClient) CollectHellos(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[HelloRequest, HelloReply], error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ stream, err := c.cc.NewStream(ctx, &GreeterService_ServiceDesc.Streams[1], GreeterService_CollectHellos_FullMethodName, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ x := &grpc.GenericClientStream[HelloRequest, HelloReply]{ClientStream: stream}
+ return x, nil
+}
+
+// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
+type GreeterService_CollectHellosClient = grpc.ClientStreamingClient[HelloRequest, HelloReply]
+
+func (c *greeterServiceClient) ChatHello(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[HelloRequest, HelloReply], error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ stream, err := c.cc.NewStream(ctx, &GreeterService_ServiceDesc.Streams[2], GreeterService_ChatHello_FullMethodName, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ x := &grpc.GenericClientStream[HelloRequest, HelloReply]{ClientStream: stream}
+ return x, nil
+}
+
+// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
+type GreeterService_ChatHelloClient = grpc.BidiStreamingClient[HelloRequest, HelloReply]
+
+// GreeterServiceServer is the server API for GreeterService service.
+// All implementations should embed UnimplementedGreeterServiceServer
+// for forward compatibility.
+//
+// The greeting service definition.
+type GreeterServiceServer interface {
+ // Sends a greeting (Unary RPC)
+ SayHello(context.Context, *HelloRequest) (*HelloReply, error)
+ // Sends multiple greetings in response to a single request (Server streaming RPC)
+ SayHelloStream(*HelloRequest, grpc.ServerStreamingServer[HelloReply]) error
+ // Collects multiple greetings and responds once (Client streaming RPC)
+ CollectHellos(grpc.ClientStreamingServer[HelloRequest, HelloReply]) error
+ // Chat with greetings back and forth (Bidirectional streaming RPC)
+ ChatHello(grpc.BidiStreamingServer[HelloRequest, HelloReply]) error
+}
+
+// UnimplementedGreeterServiceServer should be embedded to have
+// forward compatible implementations.
+//
+// NOTE: this should be embedded by value instead of pointer to avoid a nil
+// pointer dereference when methods are called.
+type UnimplementedGreeterServiceServer struct{}
+
+func (UnimplementedGreeterServiceServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
+}
+func (UnimplementedGreeterServiceServer) SayHelloStream(*HelloRequest, grpc.ServerStreamingServer[HelloReply]) error {
+ return status.Errorf(codes.Unimplemented, "method SayHelloStream not implemented")
+}
+func (UnimplementedGreeterServiceServer) CollectHellos(grpc.ClientStreamingServer[HelloRequest, HelloReply]) error {
+ return status.Errorf(codes.Unimplemented, "method CollectHellos not implemented")
+}
+func (UnimplementedGreeterServiceServer) ChatHello(grpc.BidiStreamingServer[HelloRequest, HelloReply]) error {
+ return status.Errorf(codes.Unimplemented, "method ChatHello not implemented")
+}
+func (UnimplementedGreeterServiceServer) testEmbeddedByValue() {}
+
+// UnsafeGreeterServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to GreeterServiceServer will
+// result in compilation errors.
+type UnsafeGreeterServiceServer interface {
+ mustEmbedUnimplementedGreeterServiceServer()
+}
+
+func RegisterGreeterServiceServer(s grpc.ServiceRegistrar, srv GreeterServiceServer) {
+ // If the following call pancis, it indicates UnimplementedGreeterServiceServer was
+ // embedded by pointer and is nil. This will cause panics if an
+ // unimplemented method is ever invoked, so we test this at initialization
+ // time to prevent it from happening at runtime later due to I/O.
+ if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
+ t.testEmbeddedByValue()
+ }
+ s.RegisterService(&GreeterService_ServiceDesc, srv)
+}
+
+func _GreeterService_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(HelloRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(GreeterServiceServer).SayHello(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: GreeterService_SayHello_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(GreeterServiceServer).SayHello(ctx, req.(*HelloRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _GreeterService_SayHelloStream_Handler(srv interface{}, stream grpc.ServerStream) error {
+ m := new(HelloRequest)
+ if err := stream.RecvMsg(m); err != nil {
+ return err
+ }
+ return srv.(GreeterServiceServer).SayHelloStream(m, &grpc.GenericServerStream[HelloRequest, HelloReply]{ServerStream: stream})
+}
+
+// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
+type GreeterService_SayHelloStreamServer = grpc.ServerStreamingServer[HelloReply]
+
+func _GreeterService_CollectHellos_Handler(srv interface{}, stream grpc.ServerStream) error {
+ return srv.(GreeterServiceServer).CollectHellos(&grpc.GenericServerStream[HelloRequest, HelloReply]{ServerStream: stream})
+}
+
+// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
+type GreeterService_CollectHellosServer = grpc.ClientStreamingServer[HelloRequest, HelloReply]
+
+func _GreeterService_ChatHello_Handler(srv interface{}, stream grpc.ServerStream) error {
+ return srv.(GreeterServiceServer).ChatHello(&grpc.GenericServerStream[HelloRequest, HelloReply]{ServerStream: stream})
+}
+
+// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
+type GreeterService_ChatHelloServer = grpc.BidiStreamingServer[HelloRequest, HelloReply]
+
+// GreeterService_ServiceDesc is the grpc.ServiceDesc for GreeterService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var GreeterService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "greeter.v1.GreeterService",
+ HandlerType: (*GreeterServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "SayHello",
+ Handler: _GreeterService_SayHello_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{
+ {
+ StreamName: "SayHelloStream",
+ Handler: _GreeterService_SayHelloStream_Handler,
+ ServerStreams: true,
+ },
+ {
+ StreamName: "CollectHellos",
+ Handler: _GreeterService_CollectHellos_Handler,
+ ClientStreams: true,
+ },
+ {
+ StreamName: "ChatHello",
+ Handler: _GreeterService_ChatHello_Handler,
+ ServerStreams: true,
+ ClientStreams: true,
+ },
+ },
+ Metadata: "greeter/v1/greeter.proto",
+}
+
+--- End File: grpc_agent/gen/greeter/v1/greeter_grpc.pb.go
+
+--- Start File: grpc_agent/grpc_server.go
+package main
+
+import (
+ "context"
+ pb_greeter "example/gen/greeter/v1"
+ "fmt"
+ "log"
+ "time"
+)
+
+// Server is used to implement the GreeterServiceServer.
+type Server struct {
+ pb_greeter.UnimplementedGreeterServiceServer
+}
+
+// SayHello implements the SayHello RPC method.
+func (s *Server) SayHello(ctx context.Context, req *pb_greeter.HelloRequest) (*pb_greeter.HelloReply, error) {
+ log.Printf("Received: %v", req.GetName())
+ return &pb_greeter.HelloReply{
+ Message: "Hello " + req.GetName(),
+ Timestamp: time.Now().Unix(),
+ }, nil
+}
+
+// SayHelloStream implements the server streaming RPC method.
+func (s *Server) SayHelloStream(req *pb_greeter.HelloRequest, stream pb_greeter.GreeterService_SayHelloStreamServer) error {
+ log.Printf("Received stream request: %v", req.GetName())
+
+ for i := 0; i < 5; i++ {
+ if err := stream.Send(&pb_greeter.HelloReply{
+ Message: fmt.Sprintf("Hello %s! (response #%d)", req.GetName(), i+1),
+ Timestamp: time.Now().Unix(),
+ }); err != nil {
+ return err
+ }
+ time.Sleep(100 * time.Millisecond)
+ }
+
+ return nil
+}
+
+// CollectHellos implements the client streaming RPC method.
+func (s *Server) CollectHellos(stream pb_greeter.GreeterService_CollectHellosServer) error {
+ var names []string
+
+ for {
+ req, err := stream.Recv()
+ if err != nil {
+ break
+ }
+ names = append(names, req.GetName())
+ }
+
+ var message string
+ if len(names) == 0 {
+ message = "Hello to nobody!"
+ } else if len(names) == 1 {
+ message = fmt.Sprintf("Hello to %s!", names[0])
+ } else {
+ message = fmt.Sprintf("Hello to %d people!", len(names))
+ }
+
+ return stream.SendAndClose(&pb_greeter.HelloReply{
+ Message: message,
+ Timestamp: time.Now().Unix(),
+ })
+}
+
+// ChatHello implements the bidirectional streaming RPC method.
+func (s *Server) ChatHello(stream pb_greeter.GreeterService_ChatHelloServer) error {
+ for {
+ req, err := stream.Recv()
+ if err != nil {
+ return nil
+ }
+
+ if err := stream.Send(&pb_greeter.HelloReply{
+ Message: "Hello " + req.GetName() + "!",
+ Timestamp: time.Now().Unix(),
+ }); err != nil {
+ return err
+ }
+ }
+}
+
+--- End File: grpc_agent/grpc_server.go
+
+--- Start File: grpc_agent/main.go
+package main
+
+import (
+ "context"
+ pb_greeter "example/gen/greeter/v1"
+ "flag"
+ "fmt"
+ "log/slog"
+ "net"
+ "net/http"
+ "os"
+ "os/signal"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/doganarif/govisual"
+ "github.com/doganarif/govisual/pkg/agent"
+ "github.com/doganarif/govisual/pkg/server"
+ "github.com/doganarif/govisual/pkg/store"
+ "github.com/doganarif/govisual/pkg/transport"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+)
+
+var (
+ port = flag.Int("port", 8080, "HTTP server port (for dashboard)")
+ grpcPort = flag.Int("grpc-port", 9090, "gRPC server port")
+ agentMode = flag.String("agent-mode", "store", "Agent mode: store, nats, http")
+ natsURL = flag.String("nats-url", "nats://localhost:4222", "NATS server URL. Only used with agent-mode 'nats'")
+ httpURL = flag.String("http-url", "http://localhost:8080/api/agent/logs", "HTTP endpoint URL. Only used with agent-mode 'http'")
+)
+
+func main() {
+ flag.Parse()
+
+ err := run()
+ if err != nil {
+ fmt.Printf("failed to run: %v\n", err)
+ os.Exit(1)
+ }
+}
+
+func run() error {
+ log := slog.New(slog.NewTextHandler(os.Stdout, nil))
+ sharedStore := store.NewInMemoryStore(100)
+
+ var transportObj transport.Transport
+ var err error
+ switch *agentMode {
+ case "store":
+ log.Info("Using shared store transport")
+ transportObj = transport.NewStoreTransport(sharedStore)
+ case "nats":
+ log.Info("Using NATS transport", slog.String("url", *natsURL))
+ transportObj, err = transport.NewNATSTransport(*natsURL)
+ if err != nil {
+ return fmt.Errorf("creating NATS transport: %w", err)
+ }
+ case "http":
+ log.Info("Using HTTP transport", slog.String("url", *httpURL))
+ transportObj = transport.NewHTTPTransport(*httpURL,
+ transport.WithTimeout(5*time.Second),
+ transport.WithMaxRetries(3),
+ )
+ default:
+ return fmt.Errorf("unknown agent mode %q", *agentMode)
+ }
+
+ // Create gRPC agent with the improved option system
+ grpcAgent := agent.NewGRPCAgent(transportObj,
+ agent.WithGRPCRequestDataLogging(true),
+ agent.WithGRPCResponseDataLogging(true),
+ agent.WithBatchingEnabled(true).ForGRPC(),
+ agent.WithBatchSize(5).ForGRPC(),
+ agent.WithBatchInterval(1*time.Second).ForGRPC(),
+ )
+
+ // Start the gRPC server with the agent
+ lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *grpcPort))
+ if err != nil {
+ return fmt.Errorf("listening to gRPC: %w", err)
+ }
+
+ grpcServer := server.NewGRPCServer(grpcAgent)
+ pb_greeter.RegisterGreeterServiceServer(grpcServer, &Server{})
+
+ log.Info("Starting gRPC server with visualisation", slog.Int("port", *grpcPort))
+
+ go func() {
+ err := grpcServer.Serve(lis)
+ if err != nil {
+ log.Error("failed to serve gRPC", slog.Any("err", err))
+ os.Exit(1)
+ }
+ }()
+
+ // Start the HTTP dashboard server
+ mux := http.NewServeMux()
+
+ // Add a simple homepage
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/" {
+ http.NotFound(w, r)
+ return
+ }
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte(fmt.Sprintf(`
+
+
+ GoVisual gRPC Agent Example
+
+
+
+
+
GoVisual gRPC Agent Example
+
+
Dashboard
+
Visit /__viz to access the request visualizer dashboard.
+
+
+
Configuration
+
+ gRPC Server: localhost:%d
+ Agent Mode: %s
+ %s
+
+
+
+
Test the gRPC Service
+
An initial test request has been made automatically. You can make additional requests using a gRPC client.
+
The service provides the following methods:
+
+ SayHello - Unary RPC
+ SayHelloStream - Server streaming RPC
+ CollectHellos - Client streaming RPC
+ ChatHello - Bidirectional streaming RPC
+
+
You can use a tool like grpcui or grpcurl to test these methods.
+
+
+
+
+ `, *grpcPort, *agentMode, getExtraInfo())))
+ })
+
+ // Add API endpoints for agent communication
+ if *agentMode != "store" {
+ // Create and register agent API handler
+ agentAPI := server.NewAgentAPI(sharedStore)
+ agentAPI.RegisterHandlers(mux)
+ }
+
+ // Wrap with GoVisual for dashboard
+ handler := govisual.Wrap(mux,
+ govisual.WithMaxRequests(100),
+ govisual.WithRequestBodyLogging(true),
+ govisual.WithResponseBodyLogging(true),
+ govisual.WithSharedStore(sharedStore),
+ )
+
+ // Start NATS handler if using NATS transport
+ var natsHandler *server.NATSHandler
+ if *agentMode == "nats" {
+ natsHandler, err = server.NewNATSHandler(sharedStore, *natsURL)
+ if err != nil {
+ return fmt.Errorf("creating NATS handler: %w", err)
+ }
+
+ err = natsHandler.Start()
+ if err != nil {
+ return fmt.Errorf("starting NATS handler: %w", err)
+ }
+ }
+
+ httpServer := &http.Server{
+ Addr: fmt.Sprintf(":%d", *port),
+ Handler: handler,
+ }
+
+ log.Info("Started dashboard server", slog.Int("port", *port), slog.String("dashboard_addr", fmt.Sprintf("http://localhost:%d/__viz", *port)))
+
+ go func() {
+ err := httpServer.ListenAndServe()
+ if err != nil && err != http.ErrServerClosed {
+ log.Error("failed to serve HTTP", slog.Any("err", err))
+ os.Exit(1)
+ }
+ }()
+
+ // Make test requests to show different gRPC method types
+ go func() {
+ time.Sleep(500 * time.Millisecond)
+
+ // Create gRPC client
+ conn, err := grpc.NewClient(
+ fmt.Sprintf("localhost:%d", *grpcPort),
+ grpc.WithTransportCredentials(insecure.NewCredentials()),
+ )
+ if err != nil {
+ log.Error("Failed to connect: %v", err)
+ return
+ }
+ defer conn.Close()
+
+ client := pb_greeter.NewGreeterServiceClient(conn)
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ // Test 1: Unary RPC
+ log.Info("Testing unary RPC (SayHello)")
+ resp, err := client.SayHello(ctx, &pb_greeter.HelloRequest{
+ Name: "Agent Test",
+ Message: "This is a test message",
+ })
+ if err != nil {
+ log.Error("SayHello failed", slog.Any("err", err))
+ } else {
+ log.Info("Received unary RPC response", slog.String("msg", resp.GetMessage()), slog.Int64("timestamp", resp.GetTimestamp()))
+ }
+
+ // Test 2: Server streaming RPC
+ log.Info("Testing server streaming RPC (SayHelloStream)")
+ stream, err := client.SayHelloStream(ctx, &pb_greeter.HelloRequest{
+ Name: "Stream Test",
+ Message: "Testing server streaming",
+ })
+ if err != nil {
+ log.Error("SayHelloStream failed", slog.Any("err", err))
+ } else {
+ for {
+ resp, err := stream.Recv()
+ if err != nil {
+ break
+ }
+ log.Info("Received server stream response", slog.String("msg", resp.GetMessage()), slog.Int64("timestamp", resp.GetTimestamp()))
+ }
+ }
+
+ // Test 3: Client streaming RPC
+ log.Info("Testing client streaming RPC (CollectHellos)")
+ clientStream, err := client.CollectHellos(ctx)
+ if err != nil {
+ log.Error("CollectHellos failed", slog.Any("err", err))
+ } else {
+ // Send multiple messages
+ for i := 1; i <= 3; i++ {
+ name := fmt.Sprintf("Person %d", i)
+ if err := clientStream.Send(&pb_greeter.HelloRequest{
+ Name: name,
+ Message: fmt.Sprintf("Message from %s", name),
+ }); err != nil {
+ log.Error("failed to send client stream message", slog.Any("err", err))
+ break
+ }
+ time.Sleep(100 * time.Millisecond)
+ }
+
+ // Close and receive response
+ resp, err := clientStream.CloseAndRecv()
+ if err != nil {
+ log.Error("failed to close client stream", slog.Any("err", err))
+ } else {
+ log.Info("Received client stream response", slog.String("msg", resp.GetMessage()), slog.Int64("timestamp", resp.GetTimestamp()))
+ }
+ }
+
+ // Test 4: Bidirectional streaming RPC
+ log.Info("Testing bidirectional streaming RPC (ChatHello)")
+ bidiStream, err := client.ChatHello(ctx)
+ if err != nil {
+ log.Error("ChatHello failed", slog.Any("err", err))
+ } else {
+ // Send and receive in goroutines
+ done := make(chan bool)
+
+ // Receiving goroutine
+ go func() {
+ for {
+ resp, err := bidiStream.Recv()
+ if err != nil {
+ break
+ }
+ log.Info("Received Bidi response", slog.String("msg", resp.GetMessage()), slog.Int64("timestamp", resp.GetTimestamp()))
+ }
+ done <- true
+ }()
+
+ // Send messages
+ for i := 1; i <= 3; i++ {
+ name := fmt.Sprintf("ChatPerson %d", i)
+ if err := bidiStream.Send(&pb_greeter.HelloRequest{
+ Name: name,
+ Message: fmt.Sprintf("Bidi message from %s", name),
+ }); err != nil {
+ log.Error("failed to send bidi message: %v", err)
+ break
+ }
+ time.Sleep(200 * time.Millisecond)
+ }
+
+ // Close sending
+ bidiStream.CloseSend()
+ <-done
+ }
+
+ log.Info("All gRPC tests completed")
+ }()
+
+ // Wait for termination signal
+ signalChan := make(chan os.Signal, 1)
+ signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
+ <-signalChan
+
+ log.Info("Shutdown signal received")
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ if err := httpServer.Shutdown(ctx); err != nil {
+ log.Info("HTTP server shutdown error: %v", err)
+ }
+
+ grpcServer.GracefulStop()
+ lis.Close()
+
+ if natsHandler != nil {
+ err = natsHandler.Stop()
+ if err != nil {
+ log.Error("failed to stop NATS handler", slog.Any("err", err))
+ }
+ }
+
+ err = grpcAgent.Close()
+ if err != nil {
+ log.Error("failed to close agent", slog.Any("err", err))
+ }
+
+ err = transportObj.Close()
+ if err != nil {
+ log.Error("failed to close transport", slog.Any("err", err))
+ }
+
+ err = sharedStore.Close()
+ if err != nil {
+ log.Error("failed to close store", slog.Any("err", err))
+ }
+
+ log.Info("Servers shut down successfully")
+ return nil
+}
+
+// getExtraInfo returns extra information for the homepage based on the agent mode
+func getExtraInfo() string {
+ var info strings.Builder
+
+ switch *agentMode {
+ case "store":
+ info.WriteString("Transport: In-memory shared store (direct)")
+ case "nats":
+ info.WriteString(fmt.Sprintf("Transport: NATS messaging via %s", *natsURL))
+ case "http":
+ info.WriteString(fmt.Sprintf("Transport: HTTP via %s", *httpURL))
+ }
+
+ return info.String()
+}
+
+--- End File: grpc_agent/main.go
diff --git a/cmd/examples/grpc_agent/buf.gen.yaml b/cmd/examples/grpc_agent/buf.gen.yaml
new file mode 100644
index 0000000..9b66f5e
--- /dev/null
+++ b/cmd/examples/grpc_agent/buf.gen.yaml
@@ -0,0 +1,17 @@
+version: v2
+managed:
+ enabled: true
+ override:
+ - file_option: go_package_prefix
+ value: github.com/doganarif/govisual/examples/grpc/gen
+plugins:
+ - remote: buf.build/protocolbuffers/go
+ out: gen
+ opt: paths=source_relative
+ - remote: buf.build/grpc/go
+ out: gen
+ opt:
+ - paths=source_relative
+ - require_unimplemented_servers=false
+inputs:
+ - directory: proto
diff --git a/cmd/examples/grpc_agent/buf.yaml b/cmd/examples/grpc_agent/buf.yaml
new file mode 100644
index 0000000..32c05ea
--- /dev/null
+++ b/cmd/examples/grpc_agent/buf.yaml
@@ -0,0 +1,11 @@
+version: v2
+modules:
+ - path: proto
+lint:
+ use:
+ - STANDARD
+ ignore:
+ - proto/google/type/datetime.proto
+breaking:
+ use:
+ - FILE
diff --git a/cmd/examples/grpc_agent/gen/greeter/v1/greeter.pb.go b/cmd/examples/grpc_agent/gen/greeter/v1/greeter.pb.go
new file mode 100644
index 0000000..a1884de
--- /dev/null
+++ b/cmd/examples/grpc_agent/gen/greeter/v1/greeter.pb.go
@@ -0,0 +1,207 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.36.6
+// protoc (unknown)
+// source: greeter/v1/greeter.proto
+
+package greeterv1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+ unsafe "unsafe"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// The request message containing the user's name.
+type HelloRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+ Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *HelloRequest) Reset() {
+ *x = HelloRequest{}
+ mi := &file_greeter_v1_greeter_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *HelloRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*HelloRequest) ProtoMessage() {}
+
+func (x *HelloRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_greeter_v1_greeter_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead.
+func (*HelloRequest) Descriptor() ([]byte, []int) {
+ return file_greeter_v1_greeter_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *HelloRequest) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *HelloRequest) GetMessage() string {
+ if x != nil {
+ return x.Message
+ }
+ return ""
+}
+
+// The response message containing the greetings.
+type HelloReply struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
+ Timestamp int64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *HelloReply) Reset() {
+ *x = HelloReply{}
+ mi := &file_greeter_v1_greeter_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *HelloReply) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*HelloReply) ProtoMessage() {}
+
+func (x *HelloReply) ProtoReflect() protoreflect.Message {
+ mi := &file_greeter_v1_greeter_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use HelloReply.ProtoReflect.Descriptor instead.
+func (*HelloReply) Descriptor() ([]byte, []int) {
+ return file_greeter_v1_greeter_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *HelloReply) GetMessage() string {
+ if x != nil {
+ return x.Message
+ }
+ return ""
+}
+
+func (x *HelloReply) GetTimestamp() int64 {
+ if x != nil {
+ return x.Timestamp
+ }
+ return 0
+}
+
+var File_greeter_v1_greeter_proto protoreflect.FileDescriptor
+
+const file_greeter_v1_greeter_proto_rawDesc = "" +
+ "\n" +
+ "\x18greeter/v1/greeter.proto\x12\n" +
+ "greeter.v1\"<\n" +
+ "\fHelloRequest\x12\x12\n" +
+ "\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" +
+ "\amessage\x18\x02 \x01(\tR\amessage\"D\n" +
+ "\n" +
+ "HelloReply\x12\x18\n" +
+ "\amessage\x18\x01 \x01(\tR\amessage\x12\x1c\n" +
+ "\ttimestamp\x18\x02 \x01(\x03R\ttimestamp2\xa4\x02\n" +
+ "\x0eGreeterService\x12>\n" +
+ "\bSayHello\x12\x18.greeter.v1.HelloRequest\x1a\x16.greeter.v1.HelloReply\"\x00\x12F\n" +
+ "\x0eSayHelloStream\x12\x18.greeter.v1.HelloRequest\x1a\x16.greeter.v1.HelloReply\"\x000\x01\x12E\n" +
+ "\rCollectHellos\x12\x18.greeter.v1.HelloRequest\x1a\x16.greeter.v1.HelloReply\"\x00(\x01\x12C\n" +
+ "\tChatHello\x12\x18.greeter.v1.HelloRequest\x1a\x16.greeter.v1.HelloReply\"\x00(\x010\x01B\xad\x01\n" +
+ "\x0ecom.greeter.v1B\fGreeterProtoP\x01ZDgithub.com/doganarif/govisual/examples/grpc/gen/greeter/v1;greeterv1\xa2\x02\x03GXX\xaa\x02\n" +
+ "Greeter.V1\xca\x02\n" +
+ "Greeter\\V1\xe2\x02\x16Greeter\\V1\\GPBMetadata\xea\x02\vGreeter::V1b\x06proto3"
+
+var (
+ file_greeter_v1_greeter_proto_rawDescOnce sync.Once
+ file_greeter_v1_greeter_proto_rawDescData []byte
+)
+
+func file_greeter_v1_greeter_proto_rawDescGZIP() []byte {
+ file_greeter_v1_greeter_proto_rawDescOnce.Do(func() {
+ file_greeter_v1_greeter_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_greeter_v1_greeter_proto_rawDesc), len(file_greeter_v1_greeter_proto_rawDesc)))
+ })
+ return file_greeter_v1_greeter_proto_rawDescData
+}
+
+var file_greeter_v1_greeter_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_greeter_v1_greeter_proto_goTypes = []any{
+ (*HelloRequest)(nil), // 0: greeter.v1.HelloRequest
+ (*HelloReply)(nil), // 1: greeter.v1.HelloReply
+}
+var file_greeter_v1_greeter_proto_depIdxs = []int32{
+ 0, // 0: greeter.v1.GreeterService.SayHello:input_type -> greeter.v1.HelloRequest
+ 0, // 1: greeter.v1.GreeterService.SayHelloStream:input_type -> greeter.v1.HelloRequest
+ 0, // 2: greeter.v1.GreeterService.CollectHellos:input_type -> greeter.v1.HelloRequest
+ 0, // 3: greeter.v1.GreeterService.ChatHello:input_type -> greeter.v1.HelloRequest
+ 1, // 4: greeter.v1.GreeterService.SayHello:output_type -> greeter.v1.HelloReply
+ 1, // 5: greeter.v1.GreeterService.SayHelloStream:output_type -> greeter.v1.HelloReply
+ 1, // 6: greeter.v1.GreeterService.CollectHellos:output_type -> greeter.v1.HelloReply
+ 1, // 7: greeter.v1.GreeterService.ChatHello:output_type -> greeter.v1.HelloReply
+ 4, // [4:8] is the sub-list for method output_type
+ 0, // [0:4] is the sub-list for method input_type
+ 0, // [0:0] is the sub-list for extension type_name
+ 0, // [0:0] is the sub-list for extension extendee
+ 0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_greeter_v1_greeter_proto_init() }
+func file_greeter_v1_greeter_proto_init() {
+ if File_greeter_v1_greeter_proto != nil {
+ return
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_greeter_v1_greeter_proto_rawDesc), len(file_greeter_v1_greeter_proto_rawDesc)),
+ NumEnums: 0,
+ NumMessages: 2,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_greeter_v1_greeter_proto_goTypes,
+ DependencyIndexes: file_greeter_v1_greeter_proto_depIdxs,
+ MessageInfos: file_greeter_v1_greeter_proto_msgTypes,
+ }.Build()
+ File_greeter_v1_greeter_proto = out.File
+ file_greeter_v1_greeter_proto_goTypes = nil
+ file_greeter_v1_greeter_proto_depIdxs = nil
+}
diff --git a/cmd/examples/grpc_agent/gen/greeter/v1/greeter_grpc.pb.go b/cmd/examples/grpc_agent/gen/greeter/v1/greeter_grpc.pb.go
new file mode 100644
index 0000000..bb27974
--- /dev/null
+++ b/cmd/examples/grpc_agent/gen/greeter/v1/greeter_grpc.pb.go
@@ -0,0 +1,236 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.5.1
+// - protoc (unknown)
+// source: greeter/v1/greeter.proto
+
+package greeterv1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.64.0 or later.
+const _ = grpc.SupportPackageIsVersion9
+
+const (
+ GreeterService_SayHello_FullMethodName = "/greeter.v1.GreeterService/SayHello"
+ GreeterService_SayHelloStream_FullMethodName = "/greeter.v1.GreeterService/SayHelloStream"
+ GreeterService_CollectHellos_FullMethodName = "/greeter.v1.GreeterService/CollectHellos"
+ GreeterService_ChatHello_FullMethodName = "/greeter.v1.GreeterService/ChatHello"
+)
+
+// GreeterServiceClient is the client API for GreeterService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+//
+// The greeting service definition.
+type GreeterServiceClient interface {
+ // Sends a greeting (Unary RPC)
+ SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
+ // Sends multiple greetings in response to a single request (Server streaming RPC)
+ SayHelloStream(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[HelloReply], error)
+ // Collects multiple greetings and responds once (Client streaming RPC)
+ CollectHellos(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[HelloRequest, HelloReply], error)
+ // Chat with greetings back and forth (Bidirectional streaming RPC)
+ ChatHello(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[HelloRequest, HelloReply], error)
+}
+
+type greeterServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewGreeterServiceClient(cc grpc.ClientConnInterface) GreeterServiceClient {
+ return &greeterServiceClient{cc}
+}
+
+func (c *greeterServiceClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(HelloReply)
+ err := c.cc.Invoke(ctx, GreeterService_SayHello_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *greeterServiceClient) SayHelloStream(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[HelloReply], error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ stream, err := c.cc.NewStream(ctx, &GreeterService_ServiceDesc.Streams[0], GreeterService_SayHelloStream_FullMethodName, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ x := &grpc.GenericClientStream[HelloRequest, HelloReply]{ClientStream: stream}
+ if err := x.ClientStream.SendMsg(in); err != nil {
+ return nil, err
+ }
+ if err := x.ClientStream.CloseSend(); err != nil {
+ return nil, err
+ }
+ return x, nil
+}
+
+// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
+type GreeterService_SayHelloStreamClient = grpc.ServerStreamingClient[HelloReply]
+
+func (c *greeterServiceClient) CollectHellos(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[HelloRequest, HelloReply], error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ stream, err := c.cc.NewStream(ctx, &GreeterService_ServiceDesc.Streams[1], GreeterService_CollectHellos_FullMethodName, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ x := &grpc.GenericClientStream[HelloRequest, HelloReply]{ClientStream: stream}
+ return x, nil
+}
+
+// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
+type GreeterService_CollectHellosClient = grpc.ClientStreamingClient[HelloRequest, HelloReply]
+
+func (c *greeterServiceClient) ChatHello(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[HelloRequest, HelloReply], error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ stream, err := c.cc.NewStream(ctx, &GreeterService_ServiceDesc.Streams[2], GreeterService_ChatHello_FullMethodName, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ x := &grpc.GenericClientStream[HelloRequest, HelloReply]{ClientStream: stream}
+ return x, nil
+}
+
+// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
+type GreeterService_ChatHelloClient = grpc.BidiStreamingClient[HelloRequest, HelloReply]
+
+// GreeterServiceServer is the server API for GreeterService service.
+// All implementations should embed UnimplementedGreeterServiceServer
+// for forward compatibility.
+//
+// The greeting service definition.
+type GreeterServiceServer interface {
+ // Sends a greeting (Unary RPC)
+ SayHello(context.Context, *HelloRequest) (*HelloReply, error)
+ // Sends multiple greetings in response to a single request (Server streaming RPC)
+ SayHelloStream(*HelloRequest, grpc.ServerStreamingServer[HelloReply]) error
+ // Collects multiple greetings and responds once (Client streaming RPC)
+ CollectHellos(grpc.ClientStreamingServer[HelloRequest, HelloReply]) error
+ // Chat with greetings back and forth (Bidirectional streaming RPC)
+ ChatHello(grpc.BidiStreamingServer[HelloRequest, HelloReply]) error
+}
+
+// UnimplementedGreeterServiceServer should be embedded to have
+// forward compatible implementations.
+//
+// NOTE: this should be embedded by value instead of pointer to avoid a nil
+// pointer dereference when methods are called.
+type UnimplementedGreeterServiceServer struct{}
+
+func (UnimplementedGreeterServiceServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
+}
+func (UnimplementedGreeterServiceServer) SayHelloStream(*HelloRequest, grpc.ServerStreamingServer[HelloReply]) error {
+ return status.Errorf(codes.Unimplemented, "method SayHelloStream not implemented")
+}
+func (UnimplementedGreeterServiceServer) CollectHellos(grpc.ClientStreamingServer[HelloRequest, HelloReply]) error {
+ return status.Errorf(codes.Unimplemented, "method CollectHellos not implemented")
+}
+func (UnimplementedGreeterServiceServer) ChatHello(grpc.BidiStreamingServer[HelloRequest, HelloReply]) error {
+ return status.Errorf(codes.Unimplemented, "method ChatHello not implemented")
+}
+func (UnimplementedGreeterServiceServer) testEmbeddedByValue() {}
+
+// UnsafeGreeterServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to GreeterServiceServer will
+// result in compilation errors.
+type UnsafeGreeterServiceServer interface {
+ mustEmbedUnimplementedGreeterServiceServer()
+}
+
+func RegisterGreeterServiceServer(s grpc.ServiceRegistrar, srv GreeterServiceServer) {
+ // If the following call pancis, it indicates UnimplementedGreeterServiceServer was
+ // embedded by pointer and is nil. This will cause panics if an
+ // unimplemented method is ever invoked, so we test this at initialization
+ // time to prevent it from happening at runtime later due to I/O.
+ if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
+ t.testEmbeddedByValue()
+ }
+ s.RegisterService(&GreeterService_ServiceDesc, srv)
+}
+
+func _GreeterService_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(HelloRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(GreeterServiceServer).SayHello(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: GreeterService_SayHello_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(GreeterServiceServer).SayHello(ctx, req.(*HelloRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _GreeterService_SayHelloStream_Handler(srv interface{}, stream grpc.ServerStream) error {
+ m := new(HelloRequest)
+ if err := stream.RecvMsg(m); err != nil {
+ return err
+ }
+ return srv.(GreeterServiceServer).SayHelloStream(m, &grpc.GenericServerStream[HelloRequest, HelloReply]{ServerStream: stream})
+}
+
+// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
+type GreeterService_SayHelloStreamServer = grpc.ServerStreamingServer[HelloReply]
+
+func _GreeterService_CollectHellos_Handler(srv interface{}, stream grpc.ServerStream) error {
+ return srv.(GreeterServiceServer).CollectHellos(&grpc.GenericServerStream[HelloRequest, HelloReply]{ServerStream: stream})
+}
+
+// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
+type GreeterService_CollectHellosServer = grpc.ClientStreamingServer[HelloRequest, HelloReply]
+
+func _GreeterService_ChatHello_Handler(srv interface{}, stream grpc.ServerStream) error {
+ return srv.(GreeterServiceServer).ChatHello(&grpc.GenericServerStream[HelloRequest, HelloReply]{ServerStream: stream})
+}
+
+// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
+type GreeterService_ChatHelloServer = grpc.BidiStreamingServer[HelloRequest, HelloReply]
+
+// GreeterService_ServiceDesc is the grpc.ServiceDesc for GreeterService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var GreeterService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "greeter.v1.GreeterService",
+ HandlerType: (*GreeterServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "SayHello",
+ Handler: _GreeterService_SayHello_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{
+ {
+ StreamName: "SayHelloStream",
+ Handler: _GreeterService_SayHelloStream_Handler,
+ ServerStreams: true,
+ },
+ {
+ StreamName: "CollectHellos",
+ Handler: _GreeterService_CollectHellos_Handler,
+ ClientStreams: true,
+ },
+ {
+ StreamName: "ChatHello",
+ Handler: _GreeterService_ChatHello_Handler,
+ ServerStreams: true,
+ ClientStreams: true,
+ },
+ },
+ Metadata: "greeter/v1/greeter.proto",
+}
diff --git a/cmd/examples/grpc_agent/go.mod b/cmd/examples/grpc_agent/go.mod
new file mode 100644
index 0000000..1a1328f
--- /dev/null
+++ b/cmd/examples/grpc_agent/go.mod
@@ -0,0 +1,41 @@
+module example
+
+go 1.24.0
+
+require (
+ github.com/doganarif/govisual v0.1.8
+ google.golang.org/grpc v1.72.0
+ google.golang.org/protobuf v1.36.6
+)
+
+replace github.com/doganarif/govisual => ../../../
+
+require (
+ github.com/cenkalti/backoff/v4 v4.2.1 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+ github.com/go-logr/logr v1.4.2 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-redis/redis/v8 v8.11.5 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
+ github.com/klauspost/compress v1.18.0 // indirect
+ github.com/lib/pq v1.10.9 // indirect
+ github.com/nats-io/nats.go v1.42.0 // indirect
+ github.com/nats-io/nkeys v0.4.11 // indirect
+ github.com/nats-io/nuid v1.0.1 // indirect
+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+ go.opentelemetry.io/otel v1.34.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect
+ go.opentelemetry.io/otel/metric v1.34.0 // indirect
+ go.opentelemetry.io/otel/sdk v1.34.0 // indirect
+ go.opentelemetry.io/otel/trace v1.34.0 // indirect
+ go.opentelemetry.io/proto/otlp v1.1.0 // indirect
+ golang.org/x/crypto v0.37.0 // indirect
+ golang.org/x/net v0.35.0 // indirect
+ golang.org/x/sys v0.32.0 // indirect
+ golang.org/x/text v0.24.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
+)
diff --git a/cmd/examples/grpc_agent/go.sum b/cmd/examples/grpc_agent/go.sum
new file mode 100644
index 0000000..cbb74af
--- /dev/null
+++ b/cmd/examples/grpc_agent/go.sum
@@ -0,0 +1,89 @@
+github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
+github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
+github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
+github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/nats-io/nats.go v1.42.0 h1:ynIMupIOvf/ZWH/b2qda6WGKGNSjwOUutTpWRvAmhaM=
+github.com/nats-io/nats.go v1.42.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
+github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
+github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
+github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
+github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
+github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
+github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
+github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
+go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM=
+go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
+go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
+go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
+go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
+go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
+go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
+go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
+go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
+golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
+golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
+google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
+google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
+google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
+google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/cmd/examples/grpc_agent/grpc_server.go b/cmd/examples/grpc_agent/grpc_server.go
new file mode 100644
index 0000000..bcfb1ba
--- /dev/null
+++ b/cmd/examples/grpc_agent/grpc_server.go
@@ -0,0 +1,80 @@
+package main
+
+import (
+ "context"
+ pb_greeter "example/gen/greeter/v1"
+ "fmt"
+ "time"
+)
+
+// Server is used to implement the GreeterServiceServer.
+type Server struct {
+ pb_greeter.UnimplementedGreeterServiceServer
+}
+
+// SayHello implements the SayHello RPC method.
+func (s *Server) SayHello(ctx context.Context, req *pb_greeter.HelloRequest) (*pb_greeter.HelloReply, error) {
+ return &pb_greeter.HelloReply{
+ Message: "Hello " + req.GetName(),
+ Timestamp: time.Now().Unix(),
+ }, nil
+}
+
+// SayHelloStream implements the server streaming RPC method.
+func (s *Server) SayHelloStream(req *pb_greeter.HelloRequest, stream pb_greeter.GreeterService_SayHelloStreamServer) error {
+ for i := 0; i < 5; i++ {
+ if err := stream.Send(&pb_greeter.HelloReply{
+ Message: fmt.Sprintf("Hello %s! (response #%d)", req.GetName(), i+1),
+ Timestamp: time.Now().Unix(),
+ }); err != nil {
+ return err
+ }
+ time.Sleep(100 * time.Millisecond)
+ }
+
+ return nil
+}
+
+// CollectHellos implements the client streaming RPC method.
+func (s *Server) CollectHellos(stream pb_greeter.GreeterService_CollectHellosServer) error {
+ var names []string
+
+ for {
+ req, err := stream.Recv()
+ if err != nil {
+ break
+ }
+ names = append(names, req.GetName())
+ }
+
+ var message string
+ if len(names) == 0 {
+ message = "Hello to nobody!"
+ } else if len(names) == 1 {
+ message = fmt.Sprintf("Hello to %s!", names[0])
+ } else {
+ message = fmt.Sprintf("Hello to %d people!", len(names))
+ }
+
+ return stream.SendAndClose(&pb_greeter.HelloReply{
+ Message: message,
+ Timestamp: time.Now().Unix(),
+ })
+}
+
+// ChatHello implements the bidirectional streaming RPC method.
+func (s *Server) ChatHello(stream pb_greeter.GreeterService_ChatHelloServer) error {
+ for {
+ req, err := stream.Recv()
+ if err != nil {
+ return nil
+ }
+
+ if err := stream.Send(&pb_greeter.HelloReply{
+ Message: "Hello " + req.GetName() + "!",
+ Timestamp: time.Now().Unix(),
+ }); err != nil {
+ return err
+ }
+ }
+}
diff --git a/cmd/examples/grpc_agent/main.go b/cmd/examples/grpc_agent/main.go
new file mode 100644
index 0000000..2e0c786
--- /dev/null
+++ b/cmd/examples/grpc_agent/main.go
@@ -0,0 +1,378 @@
+package main
+
+import (
+ "context"
+ pb_greeter "example/gen/greeter/v1"
+ "flag"
+ "fmt"
+ "log/slog"
+ "net"
+ "net/http"
+ "os"
+ "os/signal"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/doganarif/govisual"
+ "github.com/doganarif/govisual/pkg/agent"
+ "github.com/doganarif/govisual/pkg/server"
+ "github.com/doganarif/govisual/pkg/store"
+ "github.com/doganarif/govisual/pkg/transport"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+)
+
+var (
+ portFlaf = flag.Int("port", 8080, "HTTP server port (for dashboard)")
+ grpcPortFlag = flag.Int("grpc-port", 9090, "gRPC server port")
+ agentModeFlag = flag.String("agent-mode", "store", "Agent mode: store, nats, http")
+ natsURLFlag = flag.String("nats-url", "nats://localhost:4222", "NATS server URL. Only used with agent-mode 'nats'")
+ httpURLFlag = flag.String("http-url", "http://localhost:8080/api/agent/logs", "HTTP endpoint URL. Only used with agent-mode 'http'")
+)
+
+func main() {
+ flag.Parse()
+
+ err := run()
+ if err != nil {
+ fmt.Printf("failed to run: %v\n", err)
+ os.Exit(1)
+ }
+}
+
+func run() error {
+ port := *portFlaf
+ grpcPort := *grpcPortFlag
+ agentMode := *agentModeFlag
+ natsURL := *natsURLFlag
+ httpURL := *httpURLFlag
+
+ log := slog.New(slog.NewTextHandler(os.Stdout, nil))
+ sharedStore := store.NewInMemoryStore(100)
+
+ var transportObj transport.Transport
+ var err error
+ switch agentMode {
+ case "store":
+ log.Info("Using shared store transport")
+ transportObj = transport.NewStoreTransport(sharedStore)
+ case "nats":
+ log.Info("Using NATS transport", slog.String("url", natsURL))
+ transportObj, err = transport.NewNATSTransport(natsURL)
+ if err != nil {
+ return fmt.Errorf("creating NATS transport: %w", err)
+ }
+ case "http":
+ log.Info("Using HTTP transport", slog.String("url", httpURL))
+ transportObj = transport.NewHTTPTransport(httpURL,
+ transport.WithTimeout(5*time.Second),
+ transport.WithMaxRetries(3),
+ )
+ default:
+ return fmt.Errorf("unknown agent mode %q", agentMode)
+ }
+
+ grpcAgent := agent.NewGRPCAgent(transportObj,
+ agent.WithGRPCRequestDataLogging(true),
+ agent.WithGRPCResponseDataLogging(true),
+ agent.WithBatchSize(5).ForGRPC(),
+ agent.WithBatchInterval(1*time.Second).ForGRPC(),
+ )
+
+ lis, err := net.Listen("tcp", fmt.Sprintf(":%d", grpcPort))
+ if err != nil {
+ return fmt.Errorf("listening to gRPC: %w", err)
+ }
+
+ // This is where you can register your existing gRPC server.
+ // If you would like to take a different approach then you can use the interceptors directly as done in [server.NewGRPCServer].
+ grpcServer := server.NewGRPCServer(grpcAgent)
+ pb_greeter.RegisterGreeterServiceServer(grpcServer, &Server{})
+
+ log.Info("Starting gRPC server", slog.Int("port", grpcPort))
+
+ go func() {
+ err := grpcServer.Serve(lis)
+ if err != nil {
+ log.Error("failed to serve gRPC", slog.Any("err", err))
+ os.Exit(1)
+ }
+ }()
+
+ // Start the HTTP dashboard server with a simple homepage.
+ mux := http.NewServeMux()
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/" {
+ http.NotFound(w, r)
+ return
+ }
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte(fmt.Sprintf(`
+
+
+ GoVisual gRPC Agent Example
+
+
+
+
+
GoVisual gRPC Agent Example
+
+
Dashboard
+
Visit /__viz to access the request visualizer dashboard.
+
+
+
Configuration
+
+ gRPC Server: localhost:%d
+ Agent Mode: %s
+ %s
+
+
+
+
Test the gRPC Service
+
An initial test request has been made automatically. You can make additional requests using a gRPC client.
+
The service provides the following methods:
+
+ SayHello - Unary RPC
+ SayHelloStream - Server streaming RPC
+ CollectHellos - Client streaming RPC
+ ChatHello - Bidirectional streaming RPC
+
+
You can use a tool like grpcui or grpcurl to test these methods.
+
+
+
+
+ `, grpcPort, agentMode, getExtraInfo(agentMode, natsURL, httpURL))))
+ })
+
+ // Add API endpoints for agent communication
+ if agentMode != "store" {
+ // Create and register agent API handler
+ agentAPI := server.NewAgentAPI(sharedStore)
+ agentAPI.RegisterHandlers(mux)
+ }
+
+ // Wrap with GoVisual for dashboard
+ handler := govisual.Wrap(mux,
+ govisual.WithMaxRequests(100),
+ govisual.WithRequestBodyLogging(true),
+ govisual.WithResponseBodyLogging(true),
+ govisual.WithSharedStore(sharedStore),
+ )
+
+ // Start NATS handler if using NATS transport
+ var natsHandler *server.NATSHandler
+ if agentMode == "nats" {
+ natsHandler, err = server.NewNATSHandler(sharedStore, natsURL)
+ if err != nil {
+ return fmt.Errorf("creating NATS handler: %w", err)
+ }
+
+ err = natsHandler.Start()
+ if err != nil {
+ return fmt.Errorf("starting NATS handler: %w", err)
+ }
+ }
+
+ httpServer := &http.Server{
+ Addr: fmt.Sprintf(":%d", port),
+ Handler: handler,
+ }
+
+ log.Info("Starting HTTP server", slog.String("dashboard_addr", fmt.Sprintf("http://localhost:%d/__viz", port)))
+
+ go func() {
+ err := httpServer.ListenAndServe()
+ if err != nil && err != http.ErrServerClosed {
+ log.Error("failed to serve HTTP", slog.Any("err", err))
+ os.Exit(1)
+ }
+ }()
+
+ // Make test requests to show different gRPC method types
+ go testRPCs(log, grpcPort)
+
+ // Wait for termination signal
+ signalChan := make(chan os.Signal, 1)
+ signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
+ <-signalChan
+
+ log.Info("Shutdown signal received")
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ err = httpServer.Shutdown(ctx)
+ if err != nil {
+ log.Error("failed to shutdown server", slog.Any("err", err))
+ }
+
+ grpcServer.GracefulStop()
+ lis.Close()
+
+ if natsHandler != nil {
+ err = natsHandler.Stop()
+ if err != nil {
+ log.Error("failed to stop NATS handler", slog.Any("err", err))
+ }
+ }
+
+ err = grpcAgent.Close()
+ if err != nil {
+ log.Error("failed to close agent", slog.Any("err", err))
+ }
+
+ err = transportObj.Close()
+ if err != nil {
+ log.Error("failed to close transport", slog.Any("err", err))
+ }
+
+ err = sharedStore.Close()
+ if err != nil {
+ log.Error("failed to close store", slog.Any("err", err))
+ }
+
+ log.Info("Servers shut down successfully")
+ return nil
+}
+
+// getExtraInfo returns extra information for the homepage based on the agent mode
+func getExtraInfo(agentMode, natsURL, httpURL string) string {
+ var info strings.Builder
+
+ switch agentMode {
+ case "store":
+ info.WriteString("Transport: In-memory shared store (direct)")
+ case "nats":
+ info.WriteString(fmt.Sprintf("Transport: NATS messaging via %q", natsURL))
+ case "http":
+ info.WriteString(fmt.Sprintf("Transport: HTTP via %q", httpURL))
+ }
+
+ return info.String()
+}
+
+func testRPCs(log *slog.Logger, grpcPort int) {
+ time.Sleep(500 * time.Millisecond)
+
+ // Create gRPC client
+ conn, err := grpc.NewClient(
+ fmt.Sprintf("localhost:%d", grpcPort),
+ grpc.WithTransportCredentials(insecure.NewCredentials()),
+ )
+ if err != nil {
+ log.Error("Failed to connect: %v", err)
+ return
+ }
+ defer conn.Close()
+
+ client := pb_greeter.NewGreeterServiceClient(conn)
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ // Test 1: Unary RPC
+ log.Info("Testing unary RPC (SayHello)")
+ _, err = client.SayHello(ctx, &pb_greeter.HelloRequest{
+ Name: "Agent Test",
+ Message: "This is a test message",
+ })
+ if err != nil {
+ log.Error("failed unary RPC (SayHello)", slog.Any("err", err))
+ }
+
+ // Test 2: Server streaming RPC
+ log.Info("Testing server streaming RPC (SayHelloStream)")
+ stream, err := client.SayHelloStream(ctx, &pb_greeter.HelloRequest{
+ Name: "Stream Test",
+ Message: "Testing server streaming",
+ })
+ if err != nil {
+ log.Error("failed server streaming RPC (SayHelloStream)", slog.Any("err", err))
+ } else {
+ for {
+ _, err = stream.Recv()
+ if err != nil {
+ break
+ }
+ }
+ }
+
+ // Test 3: Client streaming RPC
+ log.Info("Testing client streaming RPC (CollectHellos)")
+ clientStream, err := client.CollectHellos(ctx)
+ if err != nil {
+ log.Error("failed client streaming RPC (CollectHellos)", slog.Any("err", err))
+ } else {
+ for i := range 3 {
+ name := fmt.Sprintf("Person %d", i)
+ err = clientStream.Send(&pb_greeter.HelloRequest{
+ Name: name,
+ Message: fmt.Sprintf("Message from %s", name),
+ })
+ if err != nil {
+ log.Error("failed to send client stream message", slog.Any("err", err))
+ break
+ }
+ time.Sleep(100 * time.Millisecond)
+ }
+
+ _, err := clientStream.CloseAndRecv()
+ if err != nil {
+ log.Error("failed to close client stream", slog.Any("err", err))
+ }
+ }
+
+ // Test 4: Bidirectional streaming RPC
+ log.Info("Testing bidirectional streaming RPC (ChatHello)")
+ bidiStream, err := client.ChatHello(ctx)
+ if err != nil {
+ log.Error("failed bidirectional streaming RPC (ChatHello)", slog.Any("err", err))
+ } else {
+ // Send and receive in goroutines
+ done := make(chan bool)
+
+ // Receiving goroutine
+ go func() {
+ for {
+ _, err := bidiStream.Recv()
+ if err != nil {
+ break
+ }
+ }
+ done <- true
+ }()
+
+ // Send messages
+ for i := range 3 {
+ name := fmt.Sprintf("Person %d", i)
+ err := bidiStream.Send(&pb_greeter.HelloRequest{
+ Name: name,
+ Message: fmt.Sprintf("Bidi message from %s", name),
+ })
+ if err != nil {
+ log.Error("failed to send bidi message: %v", err)
+ break
+ }
+ time.Sleep(200 * time.Millisecond)
+ }
+
+ // Close sending
+ bidiStream.CloseSend()
+ <-done
+ }
+
+ log.Info("All gRPC tests completed")
+}
diff --git a/cmd/examples/grpc_agent/proto/greeter/v1/greeter.proto b/cmd/examples/grpc_agent/proto/greeter/v1/greeter.proto
new file mode 100644
index 0000000..8c09405
--- /dev/null
+++ b/cmd/examples/grpc_agent/proto/greeter/v1/greeter.proto
@@ -0,0 +1,30 @@
+syntax = "proto3";
+
+package greeter.v1;
+
+// The greeting service definition.
+service GreeterService {
+ // Sends a greeting (Unary RPC)
+ rpc SayHello(HelloRequest) returns (HelloReply) {}
+
+ // Sends multiple greetings in response to a single request (Server streaming RPC)
+ rpc SayHelloStream(HelloRequest) returns (stream HelloReply) {}
+
+ // Collects multiple greetings and responds once (Client streaming RPC)
+ rpc CollectHellos(stream HelloRequest) returns (HelloReply) {}
+
+ // Chat with greetings back and forth (Bidirectional streaming RPC)
+ rpc ChatHello(stream HelloRequest) returns (stream HelloReply) {}
+}
+
+// The request message containing the user's name.
+message HelloRequest {
+ string name = 1;
+ string message = 2;
+}
+
+// The response message containing the greetings.
+message HelloReply {
+ string message = 1;
+ int64 timestamp = 2;
+}
diff --git a/cmd/examples/grpc_agent/taskfile.yaml b/cmd/examples/grpc_agent/taskfile.yaml
new file mode 100644
index 0000000..93d38d1
--- /dev/null
+++ b/cmd/examples/grpc_agent/taskfile.yaml
@@ -0,0 +1,11 @@
+version: "3"
+
+tasks:
+ generate:
+ cmds:
+ - go tool -modfile=tool.go.mod buf generate
+
+ docs:
+ cmds:
+ - go tool -modfile=tool.go.mod buf build -o example.protoset
+ - go tool -modfile=tool.go.mod grpcui -plaintext -protoset=example.protoset 127.0.0.1:9090
diff --git a/cmd/examples/grpc_agent/tool.go.mod b/cmd/examples/grpc_agent/tool.go.mod
new file mode 100644
index 0000000..9c4aa42
--- /dev/null
+++ b/cmd/examples/grpc_agent/tool.go.mod
@@ -0,0 +1,130 @@
+module example
+
+go 1.24.0
+
+tool (
+ github.com/bufbuild/buf/cmd/buf
+ github.com/fullstorydev/grpcui/cmd/grpcui
+)
+
+require (
+ buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.6-20250121211742-6d880cc6cc8d.1 // indirect
+ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250307204501-0409229c3780.1 // indirect
+ buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250408145534-f5ce355693bb.1 // indirect
+ buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.6-20250408145534-f5ce355693bb.1 // indirect
+ buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.6-20241007202033-cf42259fcbfc.1 // indirect
+ buf.build/go/bufplugin v0.8.0 // indirect
+ buf.build/go/protoyaml v0.3.2 // indirect
+ buf.build/go/spdx v0.2.0 // indirect
+ cel.dev/expr v0.23.1 // indirect
+ cloud.google.com/go/compute/metadata v0.6.0 // indirect
+ connectrpc.com/connect v1.18.1 // indirect
+ connectrpc.com/otelconnect v0.7.2 // indirect
+ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
+ github.com/Microsoft/go-winio v0.6.2 // indirect
+ github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
+ github.com/bufbuild/buf v1.53.0 // indirect
+ github.com/bufbuild/protocompile v0.14.1 // indirect
+ github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 // indirect
+ github.com/bufbuild/protovalidate-go v0.9.3 // indirect
+ github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 // indirect
+ github.com/containerd/log v0.1.0 // indirect
+ github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
+ github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
+ github.com/distribution/reference v0.6.0 // indirect
+ github.com/docker/cli v28.1.1+incompatible // indirect
+ github.com/docker/distribution v2.8.3+incompatible // indirect
+ github.com/docker/docker v28.1.1+incompatible // indirect
+ github.com/docker/docker-credential-helpers v0.9.3 // indirect
+ github.com/docker/go-connections v0.5.0 // indirect
+ github.com/docker/go-units v0.5.0 // indirect
+ github.com/envoyproxy/go-control-plane v0.13.4 // indirect
+ github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
+ github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
+ github.com/felixge/fgprof v0.9.5 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/fullstorydev/grpcui v1.4.3 // indirect
+ github.com/fullstorydev/grpcurl v1.9.3 // indirect
+ github.com/go-chi/chi/v5 v5.2.1 // indirect
+ github.com/go-logr/logr v1.4.2 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
+ github.com/gofrs/flock v0.12.1 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
+ github.com/google/cel-go v0.24.1 // indirect
+ github.com/google/go-containerregistry v0.20.3 // indirect
+ github.com/google/pprof v0.0.0-20250418163039-24c5476c6587 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/jdx/go-netrc v1.0.0 // indirect
+ github.com/jhump/protoreflect v1.17.0 // indirect
+ github.com/klauspost/compress v1.18.0 // indirect
+ github.com/klauspost/pgzip v1.2.6 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mitchellh/go-homedir v1.1.0 // indirect
+ github.com/moby/docker-image-spec v1.3.1 // indirect
+ github.com/moby/go-archive v0.1.0 // indirect
+ github.com/moby/locker v1.0.1 // indirect
+ github.com/moby/patternmatcher v0.6.0 // indirect
+ github.com/moby/sys/atomicwriter v0.1.0 // indirect
+ github.com/moby/sys/mount v0.3.4 // indirect
+ github.com/moby/sys/mountinfo v0.7.2 // indirect
+ github.com/moby/sys/reexec v0.1.0 // indirect
+ github.com/moby/sys/sequential v0.6.0 // indirect
+ github.com/moby/sys/user v0.4.0 // indirect
+ github.com/moby/sys/userns v0.1.0 // indirect
+ github.com/moby/term v0.5.2 // indirect
+ github.com/morikuni/aec v1.0.0 // indirect
+ github.com/onsi/ginkgo/v2 v2.23.4 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/opencontainers/image-spec v1.1.1 // indirect
+ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/pkg/profile v1.7.0 // indirect
+ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
+ github.com/quic-go/qpack v0.5.1 // indirect
+ github.com/quic-go/quic-go v0.51.0 // indirect
+ github.com/rs/cors v1.11.1 // indirect
+ github.com/russross/blackfriday/v2 v2.1.0 // indirect
+ github.com/segmentio/asm v1.2.0 // indirect
+ github.com/segmentio/encoding v0.4.1 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/spf13/cobra v1.9.1 // indirect
+ github.com/spf13/pflag v1.0.6 // indirect
+ github.com/stoewer/go-strcase v1.3.0 // indirect
+ github.com/tetratelabs/wazero v1.9.0 // indirect
+ github.com/vbatts/tar-split v0.12.1 // indirect
+ go.lsp.dev/jsonrpc2 v0.10.0 // indirect
+ go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 // indirect
+ go.lsp.dev/protocol v0.12.0 // indirect
+ go.lsp.dev/uri v0.3.0 // indirect
+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
+ go.opentelemetry.io/otel v1.35.0 // indirect
+ go.opentelemetry.io/otel/metric v1.35.0 // indirect
+ go.opentelemetry.io/otel/trace v1.35.0 // indirect
+ go.uber.org/automaxprocs v1.6.0 // indirect
+ go.uber.org/mock v0.5.1 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ go.uber.org/zap v1.27.0 // indirect
+ go.uber.org/zap/exp v0.3.0 // indirect
+ golang.org/x/crypto v0.37.0 // indirect
+ golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
+ golang.org/x/mod v0.24.0 // indirect
+ golang.org/x/net v0.39.0 // indirect
+ golang.org/x/oauth2 v0.25.0 // indirect
+ golang.org/x/sync v0.13.0 // indirect
+ golang.org/x/sys v0.32.0 // indirect
+ golang.org/x/term v0.31.0 // indirect
+ golang.org/x/text v0.24.0 // indirect
+ golang.org/x/tools v0.32.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
+ google.golang.org/grpc v1.71.0 // indirect
+ google.golang.org/protobuf v1.36.6 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ pluginrpc.com/pluginrpc v0.5.0 // indirect
+)
diff --git a/cmd/examples/grpc_agent/tool.go.sum b/cmd/examples/grpc_agent/tool.go.sum
new file mode 100644
index 0000000..1d65cbd
--- /dev/null
+++ b/cmd/examples/grpc_agent/tool.go.sum
@@ -0,0 +1,309 @@
+buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.6-20250121211742-6d880cc6cc8d.1 h1:f6miF8tK6H+Ktad24WpnNfpHO75GRGk0rhJ1mxPXqgA=
+buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.6-20250121211742-6d880cc6cc8d.1/go.mod h1:rvbyamNtvJ4o3ExeCmaG5/6iHnu0vy0E+UQ+Ph0om8s=
+buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250307204501-0409229c3780.1 h1:zgJPqo17m28+Lf5BW4xv3PvU20BnrmTcGYrog22lLIU=
+buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250307204501-0409229c3780.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U=
+buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250408145534-f5ce355693bb.1 h1:T9SjIDu/MDc0w3Zsn2vTY2cAln9smvALy/x6hifEoB4=
+buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250408145534-f5ce355693bb.1/go.mod h1:oPjPPfT42x42QaEpI3YoHfgcVXRg9WMZ3e0qg34HT1g=
+buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.6-20250408145534-f5ce355693bb.1 h1:mduBaTj1Vc2XKmO5fRdpxk4K96cKERaV6nFPMHyLy6M=
+buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.6-20250408145534-f5ce355693bb.1/go.mod h1:ee69ieBAzwc/oY/Vde0K4r6JWvrk093q4Z/FXexPMmA=
+buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.6-20241007202033-cf42259fcbfc.1 h1:trcsXBDm8exui7mvndZnvworCyBq1xuMnod2N0j79K8=
+buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.6-20241007202033-cf42259fcbfc.1/go.mod h1:OUbhXurY+VHFGn9FBxcRy8UB7HXk9NvJ2qCgifOMypQ=
+buf.build/go/bufplugin v0.8.0 h1:YgR1+CNGmzR69jt85oRWTa5FioZoX/tOrHV+JxfNnnk=
+buf.build/go/bufplugin v0.8.0/go.mod h1:rcm0Esd3P/GM2rtYTvz3+9Gf8w9zdo7rG8dKSxYHHIE=
+buf.build/go/protoyaml v0.3.2 h1:QJF3k7btMameIadLLcK3Rry81OK3gYA5nZMXirV1Bs4=
+buf.build/go/protoyaml v0.3.2/go.mod h1:rUlMqwfZeONS/BAt00wB6jV5ay/eHXUzxgiKSIyrvyc=
+buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw=
+buf.build/go/spdx v0.2.0/go.mod h1:bXdwQFem9Si3nsbNy8aJKGPoaPi5DKwdeEp5/ArZ6w8=
+cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg=
+cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
+cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
+cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
+cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
+connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw=
+connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
+connectrpc.com/otelconnect v0.7.2 h1:WlnwFzaW64dN06JXU+hREPUGeEzpz3Acz2ACOmN8cMI=
+connectrpc.com/otelconnect v0.7.2/go.mod h1:JS7XUKfuJs2adhCnXhNHPHLz6oAaZniCJdSF00OZSew=
+github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
+github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
+github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
+github.com/bufbuild/buf v1.53.0 h1:i0OgpDkzv8yLyCokXRCgbQao/SqTnqXYtERre0Jw6Do=
+github.com/bufbuild/buf v1.53.0/go.mod h1:DylfLDMblZt5mX/SEFv2VzZMgdlePArhMGYVpk4M6N0=
+github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
+github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
+github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU=
+github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
+github.com/bufbuild/protovalidate-go v0.9.3 h1:XvdtwQuppS3wjzGfpOirsqwN5ExH2+PiIuA/XZd3MTM=
+github.com/bufbuild/protovalidate-go v0.9.3/go.mod h1:2lUDP6fNd3wxznRNH3Nj64VB07+PySeslamkerwP6tE=
+github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
+github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
+github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
+github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
+github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 h1:boJj011Hh+874zpIySeApCX4GeOjPl9qhRF3QuIZq+Q=
+github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
+github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
+github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
+github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
+github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
+github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k=
+github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
+github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I=
+github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
+github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
+github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
+github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
+github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
+github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
+github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
+github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
+github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
+github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
+github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
+github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fullstorydev/grpcui v1.4.3 h1:Nu8X2sqnw9Hk/K6MxnxC16WHCVfD5T9GAnxK/GBcr6A=
+github.com/fullstorydev/grpcui v1.4.3/go.mod h1:MFRnL00NjgWlNA0yrsFyEe7FvOYAENu5jxRsHx4fKC0=
+github.com/fullstorydev/grpcurl v1.9.3 h1:PC1Xi3w+JAvEE2Tg2Gf2RfVgPbf9+tbuQr1ZkyVU3jk=
+github.com/fullstorydev/grpcurl v1.9.3/go.mod h1:/b4Wxe8bG6ndAjlfSUjwseQReUDUvBJiFEB7UllOlUE=
+github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
+github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
+github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
+github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
+github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/cel-go v0.24.1 h1:jsBCtxG8mM5wiUJDSGUqU0K7Mtr3w7Eyv00rw4DiZxI=
+github.com/google/cel-go v0.24.1/go.mod h1:Hdf9TqOaTNSFQA1ybQaRqATVoK7m/zcf7IMhGXP5zI8=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
+github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
+github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
+github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
+github.com/google/pprof v0.0.0-20250418163039-24c5476c6587 h1:b/8HpQhvKLSNzH5oTXN2WkNcMl6YB5K3FRbb+i+Ml34=
+github.com/google/pprof v0.0.0-20250418163039-24c5476c6587/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
+github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ=
+github.com/jdx/go-netrc v1.0.0/go.mod h1:Gh9eFQJnoTNIRHXl2j5bJXA1u84hQWJWgGh569zF3v8=
+github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
+github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
+github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
+github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
+github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
+github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
+github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
+github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
+github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
+github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
+github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
+github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
+github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
+github.com/moby/sys/mount v0.3.4 h1:yn5jq4STPztkkzSKpZkLcmjue+bZJ0u2AuQY1iNI1Ww=
+github.com/moby/sys/mount v0.3.4/go.mod h1:KcQJMbQdJHPlq5lcYT+/CjatWM4PuxKe+XLSVS4J6Os=
+github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
+github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
+github.com/moby/sys/reexec v0.1.0 h1:RrBi8e0EBTLEgfruBOFcxtElzRGTEUkeIFaVXgU7wok=
+github.com/moby/sys/reexec v0.1.0/go.mod h1:EqjBg8F3X7iZe5pU6nRZnYCMUTXoxsjiIfHup5wYIN8=
+github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
+github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
+github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
+github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
+github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
+github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
+github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
+github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
+github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
+github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
+github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
+github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
+github.com/quic-go/quic-go v0.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc=
+github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
+github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
+github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
+github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
+github.com/segmentio/encoding v0.4.1 h1:KLGaLSW0jrmhB58Nn4+98spfvPvmo4Ci1P/WIQ9wn7w=
+github.com/segmentio/encoding v0.4.1/go.mod h1:/d03Cd8PoaDeceuhUUUQWjU0KhWjrmYrWPgtJHYZSnI=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
+github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
+github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
+github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
+github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
+github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.lsp.dev/jsonrpc2 v0.10.0 h1:Pr/YcXJoEOTMc/b6OTmcR1DPJ3mSWl/SWiU1Cct6VmI=
+go.lsp.dev/jsonrpc2 v0.10.0/go.mod h1:fmEzIdXPi/rf6d4uFcayi8HpFP1nBF99ERP1htC72Ac=
+go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 h1:hCzQgh6UcwbKgNSRurYWSqh8MufqRRPODRBblutn4TE=
+go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2/go.mod h1:gtSHRuYfbCT0qnbLnovpie/WEmqyJ7T4n6VXiFMBtcw=
+go.lsp.dev/protocol v0.12.0 h1:tNprUI9klQW5FAFVM4Sa+AbPFuVQByWhP1ttNUAjIWg=
+go.lsp.dev/protocol v0.12.0/go.mod h1:Qb11/HgZQ72qQbeyPfJbu3hZBH23s1sr4st8czGeDMQ=
+go.lsp.dev/uri v0.3.0 h1:KcZJmh6nFIBeJzTugn5JTU6OOyG0lDOo3R9KwTxTYbo=
+go.lsp.dev/uri v0.3.0/go.mod h1:P5sbO1IQR+qySTWOCnhnK7phBx+W3zbLqSMDJNTw88I=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
+go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
+go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
+go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
+go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
+go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
+golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
+golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
+golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
+golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
+golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
+golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
+golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e h1:UdXH7Kzbj+Vzastr5nVfccbmFsmYNygVLSPk1pEfDoY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e/go.mod h1:085qFyf2+XaZlRdCgKNCIZ3afY2p4HHZdoIRpId8F4A=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
+google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+pluginrpc.com/pluginrpc v0.5.0 h1:tOQj2D35hOmvHyPu8e7ohW2/QvAnEtKscy2IJYWQ2yo=
+pluginrpc.com/pluginrpc v0.5.0/go.mod h1:UNWZ941hcVAoOZUn8YZsMmOZBzbUjQa3XMns8RQLp9o=
diff --git a/go.mod b/go.mod
index bac7f14..8466a29 100644
--- a/go.mod
+++ b/go.mod
@@ -5,13 +5,14 @@ go 1.24.0
require (
github.com/go-redis/redis/v8 v8.11.5
github.com/lib/pq v1.10.9
- github.com/ncruces/go-sqlite3 v0.25.1
+ github.com/mattn/go-sqlite3 v1.14.28
+ github.com/nats-io/nats.go v1.42.0
go.opentelemetry.io/otel v1.24.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0
go.opentelemetry.io/otel/sdk v1.24.0
go.opentelemetry.io/otel/trace v1.24.0
- google.golang.org/grpc v1.62.1
+ google.golang.org/grpc v1.64.1
)
require (
@@ -20,17 +21,17 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
- github.com/golang/protobuf v1.5.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
- github.com/mattn/go-sqlite3 v1.14.28 // indirect
- github.com/ncruces/julianday v1.0.0 // indirect
- github.com/tetratelabs/wazero v1.9.0 // indirect
+ github.com/klauspost/compress v1.18.0 // indirect
+ github.com/nats-io/nkeys v0.4.11 // indirect
+ github.com/nats-io/nuid v1.0.1 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
- golang.org/x/net v0.20.0 // indirect
+ golang.org/x/crypto v0.37.0 // indirect
+ golang.org/x/net v0.26.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect
- google.golang.org/protobuf v1.32.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
+ google.golang.org/protobuf v1.33.0 // indirect
)
diff --git a/go.sum b/go.sum
index efbd483..66c6427 100644
--- a/go.sum
+++ b/go.sum
@@ -15,22 +15,22 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
-github.com/ncruces/go-sqlite3 v0.25.1 h1:nRK2mZ0jLNFJco8QFZ9+dCXxOGe6Re8bbG5o8gyalr8=
-github.com/ncruces/go-sqlite3 v0.25.1/go.mod h1:4BtkHRLbX5hE0PhBxJ11qETTwL7M4lk0ttru9nora1E=
-github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
-github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
+github.com/nats-io/nats.go v1.42.0 h1:ynIMupIOvf/ZWH/b2qda6WGKGNSjwOUutTpWRvAmhaM=
+github.com/nats-io/nats.go v1.42.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
+github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
+github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
+github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
+github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
@@ -41,8 +41,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
-github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
@@ -59,25 +57,22 @@ go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxi
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
-golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
-golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
+golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
+golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
+golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ=
-google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro=
-google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1:Lj5rbfG876hIAYFjqiJnPHfhXbv+nzTWfm04Fg/XSVU=
-google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
-google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
-google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
-google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=
+google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
+google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
+google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
+google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
diff --git a/internal/constants.go b/internal/constants.go
new file mode 100644
index 0000000..2f1aaf0
--- /dev/null
+++ b/internal/constants.go
@@ -0,0 +1,7 @@
+package internal
+
+const (
+ NatsSubjectSingleLogMessages = "govisual.logs.single"
+ NatsSubjectBatchLogMessages = "govisual.logs.batch"
+ NatsSubjectAgentStatusMessages = "govisual.agent.status"
+)
diff --git a/internal/dashboard/handler.go b/internal/dashboard/handler.go
index 0f26a0e..7dabc78 100644
--- a/internal/dashboard/handler.go
+++ b/internal/dashboard/handler.go
@@ -12,7 +12,7 @@ import (
"strings"
"time"
- "github.com/doganarif/govisual/internal/store"
+ "github.com/doganarif/govisual/pkg/store"
)
// Handler is the HTTP handler for the dashboard
diff --git a/internal/dashboard/templates/components/filters.html b/internal/dashboard/templates/components/filters.html
index b39ff40..bd54b36 100644
--- a/internal/dashboard/templates/components/filters.html
+++ b/internal/dashboard/templates/components/filters.html
@@ -27,25 +27,24 @@ Filters
Path Contains
-
+
Min Duration (ms)
-
+
+
+ Request Type
+
+ All Types
+ HTTP
+ gRPC
+
+
+
Apply Filters
Reset
@@ -54,4 +53,4 @@
Filters
-{{end}}
+{{end}}
\ No newline at end of file
diff --git a/internal/dashboard/templates/components/grpc-details.html b/internal/dashboard/templates/components/grpc-details.html
new file mode 100644
index 0000000..4ed5711
--- /dev/null
+++ b/internal/dashboard/templates/components/grpc-details.html
@@ -0,0 +1,60 @@
+{{define "grpc-details"}}
+
+
+
+
+ ID:
+
+
+ Time:
+
+
+ Service:
+
+
+ Method:
+
+
+ Type:
+
+
+ Peer:
+
+
+ Status:
+
+
+ Duration:
+
+
+
+
+
Request Metadata
+
+
+
Response Metadata
+
+
+
+
+
+
+
+
+
Close Details
+
+{{end}}
\ No newline at end of file
diff --git a/internal/dashboard/templates/components/stats.html b/internal/dashboard/templates/components/stats.html
index 83d16f4..3569d98 100644
--- a/internal/dashboard/templates/components/stats.html
+++ b/internal/dashboard/templates/components/stats.html
@@ -27,5 +27,23 @@ Request Statistics
Avg Response Time
+
+
+
gRPC Request Statistics
+
+
+
0
+
Total gRPC Requests
+
+
+
+
0
+
Errors (Non-Zero)
+
+
+
-{{end}}
+{{end}}
\ No newline at end of file
diff --git a/internal/dashboard/templates/dashboard.html b/internal/dashboard/templates/dashboard.html
index 9d31ef6..ff0d19b 100644
--- a/internal/dashboard/templates/dashboard.html
+++ b/internal/dashboard/templates/dashboard.html
@@ -71,32 +71,70 @@
function applyFilters() {
const methodFilter = document.getElementById("method-filter").value;
const statusFilter = document.getElementById("status-filter").value;
- const pathFilter = document
- .getElementById("path-filter")
- .value.toLowerCase();
+ const pathFilter = document.getElementById("path-filter").value.toLowerCase();
const durationFilter = document.getElementById("duration-filter").value;
+ const typeFilter = document.getElementById("type-filter").value;
filteredRequests = allRequests.filter((req) => {
- // Method filter
- if (methodFilter && req.Method !== methodFilter) {
+ // Type filter (HTTP vs gRPC)
+ if (typeFilter && req.Type !== typeFilter) {
return false;
}
+ // Method filter
+ if (methodFilter) {
+ if (req.Type === "grpc") {
+ // For gRPC, we filter on method type (unary, client_stream, etc.)
+ if (req.GRPCMethodType !== methodFilter) {
+ return false;
+ }
+ } else {
+ // For HTTP, we filter on HTTP method (GET, POST, etc.)
+ if (req.Method !== methodFilter) {
+ return false;
+ }
+ }
+ }
+
// Status code filter
if (statusFilter) {
- const firstDigit = Math.floor(req.StatusCode / 100);
- if (statusFilter === "2xx" && firstDigit !== 2) return false;
- if (statusFilter === "3xx" && firstDigit !== 3) return false;
- if (statusFilter === "4xx" && firstDigit !== 4) return false;
- if (statusFilter === "5xx" && firstDigit !== 5) return false;
+ if (req.Type === "grpc") {
+ // gRPC status codes are handled differently
+ if (statusFilter === "success" && req.GRPCStatusCode !== 0) {
+ return false;
+ } else if (statusFilter === "error" && req.GRPCStatusCode === 0) {
+ return false;
+ } else if (statusFilter.startsWith("code-")) {
+ const code = parseInt(statusFilter.substring(5));
+ if (req.GRPCStatusCode !== code) {
+ return false;
+ }
+ }
+ } else {
+ // HTTP status codes
+ const firstDigit = Math.floor(req.StatusCode / 100);
+ if (statusFilter === "2xx" && firstDigit !== 2) return false;
+ if (statusFilter === "3xx" && firstDigit !== 3) return false;
+ if (statusFilter === "4xx" && firstDigit !== 4) return false;
+ if (statusFilter === "5xx" && firstDigit !== 5) return false;
+ }
}
- // Path filter
- if (pathFilter && !req.Path.toLowerCase().includes(pathFilter)) {
- return false;
+ // Path/service filter
+ if (pathFilter) {
+ if (req.Type === "grpc") {
+ const servicePath = `${req.GRPCService}/${req.GRPCMethod}`.toLowerCase();
+ if (!servicePath.includes(pathFilter)) {
+ return false;
+ }
+ } else {
+ if (!req.Path.toLowerCase().includes(pathFilter)) {
+ return false;
+ }
+ }
}
- // Duration filter
+ // Duration filter - same for both types
if (
durationFilter &&
(!req.Duration || req.Duration < parseInt(durationFilter))
@@ -246,6 +284,8 @@
sortedRequests.forEach((req) => {
const row = document.createElement("tr");
row.setAttribute("data-id", req.ID);
+ row.setAttribute("data-type", req.Type); // Add type attribute for filtering
+
row.addEventListener("click", function (e) {
// Don't navigate if clicking on a checkbox or its container
if (
@@ -258,35 +298,70 @@
// Navigate to request details section and show details
document.querySelector('[data-target="requests-section"]').click();
- showRequestDetails(req);
- showRequestTrace(req);
+
+ // Show appropriate details based on request type
+ if (req.Type === "grpc") {
+ showGRPCRequestDetails(req);
+ } else {
+ showRequestDetails(req);
+ showRequestTrace(req);
+ }
});
const timeCell = document.createElement("td");
timeCell.textContent = new Date(req.Timestamp).toLocaleTimeString();
row.appendChild(timeCell);
+ const typeCell = document.createElement("td");
+ if (req.Type === "grpc") {
+ typeCell.textContent = "gRPC";
+ typeCell.className = "type-grpc";
+ row.classList.add("grpc-request"); // Add class for styling
+ } else {
+ typeCell.textContent = "HTTP";
+ typeCell.className = "type-http";
+ }
+ row.appendChild(typeCell);
+
const methodCell = document.createElement("td");
- methodCell.textContent = req.Method;
- // Add CSS class based on method
- methodCell.className = "method-" + req.Method.toLowerCase();
+ if (req.Type === "grpc") {
+ methodCell.textContent = req.GRPCMethodType || "UNARY";
+ methodCell.className = "method-grpc";
+ } else {
+ methodCell.textContent = req.Method;
+ methodCell.className = "method-" + req.Method.toLowerCase();
+ }
row.appendChild(methodCell);
const pathCell = document.createElement("td");
- pathCell.textContent = req.Path + (req.Query ? "?" + req.Query : "");
+ if (req.Type === "grpc") {
+ pathCell.textContent = `${req.GRPCService}/${req.GRPCMethod}`;
+ } else {
+ pathCell.textContent = req.Path + (req.Query ? "?" + req.Query : "");
+ }
row.appendChild(pathCell);
const statusCell = document.createElement("td");
- statusCell.textContent = req.StatusCode;
- // Add CSS class based on status code
- if (req.StatusCode >= 200 && req.StatusCode < 300) {
- statusCell.className = "status-success";
- } else if (req.StatusCode >= 300 && req.StatusCode < 400) {
- statusCell.className = "status-redirect";
- } else if (req.StatusCode >= 400 && req.StatusCode < 500) {
- statusCell.className = "status-client-error";
- } else if (req.StatusCode >= 500) {
- statusCell.className = "status-server-error";
+ if (req.Type === "grpc") {
+ statusCell.textContent = req.GRPCStatusCode;
+ // Add CSS class based on gRPC status code
+ if (req.GRPCStatusCode === 0) {
+ statusCell.className = "status-success";
+ } else {
+ statusCell.className = "status-client-error";
+ }
+ } else {
+ statusCell.textContent = req.StatusCode;
+ // Add CSS class based on HTTP status code
+ if (req.StatusCode >= 200 && req.StatusCode < 300) {
+ statusCell.className = "status-success";
+ } else if (req.StatusCode >= 300 && req.StatusCode < 400) {
+ statusCell.className = "status-redirect";
+ } else if (req.StatusCode >= 400 && req.StatusCode < 500) {
+ statusCell.className = "status-client-error";
+ } else if (req.StatusCode >= 500) {
+ statusCell.className = "status-server-error";
+ }
}
row.appendChild(statusCell);
@@ -308,6 +383,211 @@
});
}
+ // Function to show gRPC request details
+ function showGRPCRequestDetails(req) {
+ selectedRequestId = req.ID;
+
+ // Hide HTTP-specific trace view
+ document.getElementById("traceContainer").style.display = "none";
+
+ // Show gRPC details container
+ const grpcDetailsContainer = document.getElementById("grpcRequestDetails");
+ if (!grpcDetailsContainer) {
+ // Create container if it doesn't exist yet
+ createGRPCDetailsContainer();
+ }
+
+ document.getElementById("grpcRequestDetails").style.display = "block";
+ document.getElementById("requestDetails").style.display = "none"; // Hide HTTP details
+
+ // Fill in basic request information
+ document.getElementById("grpc-detail-id").textContent = req.ID;
+ document.getElementById("grpc-detail-time").textContent = new Date(
+ req.Timestamp
+ ).toLocaleString();
+ document.getElementById("grpc-detail-service").textContent = req.GRPCService;
+ document.getElementById("grpc-detail-method").textContent = req.GRPCMethod;
+ document.getElementById("grpc-detail-type").textContent = req.GRPCMethodType;
+ document.getElementById("grpc-detail-peer").textContent = req.GRPCPeer || "N/A";
+
+ // Status and error handling
+ const statusElement = document.getElementById("grpc-detail-status");
+ statusElement.textContent = req.GRPCStatusCode + (req.GRPCStatusDesc ? ` (${req.GRPCStatusDesc})` : "");
+
+ // Apply status styling
+ if (req.GRPCStatusCode === 0) {
+ statusElement.className = "status-success";
+ } else {
+ statusElement.className = "status-client-error";
+ }
+
+ document.getElementById("grpc-detail-duration").textContent =
+ req.Duration !== undefined && req.Duration !== null
+ ? req.Duration + " ms"
+ : "N/A";
+
+ // Handle error display
+ const errorContainer = document.getElementById("grpc-error-container");
+ if (req.Error) {
+ errorContainer.style.display = "block";
+ document.getElementById("grpc-detail-error").textContent = req.Error;
+ } else {
+ errorContainer.style.display = "none";
+ }
+
+ // Format and display metadata
+ const requestMDElement = document.getElementById("grpc-detail-request-md");
+ requestMDElement.textContent = formatGRPCMetadata(req.GRPCRequestMD);
+
+ const responseMDElement = document.getElementById("grpc-detail-response-md");
+ responseMDElement.textContent = formatGRPCMetadata(req.GRPCResponseMD);
+
+ // Handle request and response data
+ const requestDataContainer = document.getElementById("grpc-request-data-container");
+ const requestDataElement = document.getElementById("grpc-detail-request-data");
+
+ if (req.GRPCRequestData) {
+ requestDataContainer.style.display = "block";
+ requestDataElement.textContent = formatBody(req.GRPCRequestData);
+ } else {
+ requestDataContainer.style.display = "none";
+ }
+
+ const responseDataContainer = document.getElementById("grpc-response-data-container");
+ const responseDataElement = document.getElementById("grpc-detail-response-data");
+
+ if (req.GRPCResponseData) {
+ responseDataContainer.style.display = "block";
+ responseDataElement.textContent = formatBody(req.GRPCResponseData);
+ } else {
+ responseDataContainer.style.display = "none";
+ }
+
+ // Handle streaming messages if present
+ const messagesContainer = document.getElementById("grpc-messages-container");
+ const messagesElement = document.getElementById("grpc-messages-list");
+
+ if (req.GRPCMessages && req.GRPCMessages.length > 0) {
+ messagesContainer.style.display = "block";
+ messagesElement.innerHTML = ""; // Clear previous messages
+
+ // Create a timeline of messages
+ req.GRPCMessages.forEach((msg, index) => {
+ const msgItem = document.createElement("div");
+ msgItem.className = `grpc-message ${msg.Direction === "sent" ? "message-sent" : "message-received"}`;
+
+ const msgHeader = document.createElement("div");
+ msgHeader.className = "grpc-message-header";
+ msgHeader.innerHTML = `#${index + 1} ${msg.Direction.toUpperCase()} ${new Date(msg.Timestamp).toLocaleTimeString()}.${new Date(msg.Timestamp).getMilliseconds()} `;
+
+ const msgContent = document.createElement("pre");
+ msgContent.className = "grpc-message-content";
+ msgContent.textContent = formatBody(msg.Data);
+
+ msgItem.appendChild(msgHeader);
+ msgItem.appendChild(msgContent);
+ messagesElement.appendChild(msgItem);
+ });
+ } else {
+ messagesContainer.style.display = "none";
+ }
+ }
+
+ // Helper function to format GRPC metadata
+ function formatGRPCMetadata(metadata) {
+ if (!metadata || Object.keys(metadata).length === 0) {
+ return "No metadata available";
+ }
+
+ let result = "";
+ for (const key in metadata) {
+ result += `${key}: ${metadata[key].join(", ")}\n`;
+ }
+ return result;
+ }
+
+ // Function to create the gRPC details container if needed
+ function createGRPCDetailsContainer() {
+ // Create the container element
+ const container = document.createElement("div");
+ container.id = "grpcRequestDetails";
+ container.className = "card";
+ container.style.display = "none";
+
+ // Add the content
+ container.innerHTML = `
+
+
+
+ ID:
+
+
+ Time:
+
+
+ Service:
+
+
+ Method:
+
+
+ Type:
+
+
+ Peer:
+
+
+ Status:
+
+
+ Duration:
+
+
+
+
+
Request Metadata
+
+
+
Response Metadata
+
+
+
+
+
+
+
+
+ Close Details
+ `;
+
+ // Add the container to the requests section
+ const requestsSection = document.getElementById("requests-section");
+ requestsSection.appendChild(container);
+
+ // Add event listener for close button
+ document.getElementById("close-grpc-details").addEventListener("click", () => {
+ document.getElementById("grpcRequestDetails").style.display = "none";
+ selectedRequestId = null;
+
+ // Return to dashboard section
+ document.querySelector('[data-target="dashboard-section"]').click();
+ });
+ }
+
// Update statistics
function updateStats(requests) {
if (!requests || requests.length === 0) {
@@ -316,6 +596,9 @@
document.getElementById("stat-redirect").textContent = "0";
document.getElementById("stat-client-error").textContent = "0";
document.getElementById("stat-server-error").textContent = "0";
+ document.getElementById("stat-grpc-total").textContent = "0";
+ document.getElementById("stat-grpc-success").textContent = "0";
+ document.getElementById("stat-grpc-error").textContent = "0";
document.getElementById("stat-avg-time").textContent = "0 ms";
// Update sidebar stats
@@ -323,18 +606,41 @@
return;
}
- let success = 0;
- let redirect = 0;
- let clientError = 0;
- let serverError = 0;
+ // HTTP stats
+ let httpSuccess = 0;
+ let httpRedirect = 0;
+ let httpClientError = 0;
+ let httpServerError = 0;
+ let httpTotal = 0;
+
+ // gRPC stats
+ let grpcSuccess = 0;
+ let grpcError = 0;
+ let grpcTotal = 0;
+
+ // Shared stats
let totalDuration = 0;
let validDurationCount = 0;
requests.forEach((req) => {
- if (req.StatusCode >= 200 && req.StatusCode < 300) success++;
- else if (req.StatusCode >= 300 && req.StatusCode < 400) redirect++;
- else if (req.StatusCode >= 400 && req.StatusCode < 500) clientError++;
- else if (req.StatusCode >= 500) serverError++;
+ if (req.Type === "grpc") {
+ // gRPC request
+ grpcTotal++;
+
+ if (req.GRPCStatusCode === 0) {
+ grpcSuccess++;
+ } else {
+ grpcError++;
+ }
+ } else {
+ // HTTP request
+ httpTotal++;
+
+ if (req.StatusCode >= 200 && req.StatusCode < 300) httpSuccess++;
+ else if (req.StatusCode >= 300 && req.StatusCode < 400) httpRedirect++;
+ else if (req.StatusCode >= 400 && req.StatusCode < 500) httpClientError++;
+ else if (req.StatusCode >= 500) httpServerError++;
+ }
if (req.Duration !== undefined && req.Duration !== null) {
totalDuration += req.Duration;
@@ -347,18 +653,312 @@
? Math.round(totalDuration / validDurationCount)
: 0;
- const successRate =
- requests.length > 0 ? Math.round((success / requests.length) * 100) : 0;
+ // Calculate combined success rate (HTTP + gRPC)
+ const successCount = httpSuccess + grpcSuccess;
+ const totalCount = requests.length;
+ const successRate = totalCount > 0 ? Math.round((successCount / totalCount) * 100) : 0;
+
+ // Update HTTP stats
+ document.getElementById("stat-total").textContent = httpTotal;
+ document.getElementById("stat-success").textContent = httpSuccess;
+ document.getElementById("stat-redirect").textContent = httpRedirect;
+ document.getElementById("stat-client-error").textContent = httpClientError;
+ document.getElementById("stat-server-error").textContent = httpServerError;
- document.getElementById("stat-total").textContent = requests.length;
- document.getElementById("stat-success").textContent = success;
- document.getElementById("stat-redirect").textContent = redirect;
- document.getElementById("stat-client-error").textContent = clientError;
- document.getElementById("stat-server-error").textContent = serverError;
+ // Update gRPC stats
+ document.getElementById("stat-grpc-total").textContent = grpcTotal;
+ document.getElementById("stat-grpc-success").textContent = grpcSuccess;
+ document.getElementById("stat-grpc-error").textContent = grpcError;
+
+ // Update shared stats
document.getElementById("stat-avg-time").textContent = avgDuration + " ms";
// Update sidebar stats
- updateSidebarStats(requests.length, successRate, avgDuration);
+ updateSidebarStats(totalCount, successRate, avgDuration);
+ }
+
+ function addGRPCStyles() {
+ const styleElement = document.createElement('style');
+ styleElement.textContent = `
+ /* gRPC row styling */
+ tr.grpc-request {
+ background-color: rgba(76, 175, 80, 0.05);
+ }
+
+ tr.grpc-request:hover {
+ background-color: rgba(76, 175, 80, 0.1);
+ }
+
+ /* Type indicators */
+ .type-grpc {
+ color: #4CAF50;
+ font-weight: bold;
+ }
+
+ .type-http {
+ color: #2196F3;
+ font-weight: bold;
+ }
+
+ /* Method types */
+ .method-grpc {
+ color: #9C27B0;
+ font-style: italic;
+ }
+
+ /* gRPC stream messages */
+ .grpc-messages {
+ max-height: 400px;
+ overflow-y: auto;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ margin-top: 10px;
+ }
+
+ .grpc-message {
+ border-bottom: 1px solid var(--border-color);
+ margin-bottom: 10px;
+ padding-bottom: 10px;
+ }
+
+ .grpc-message:last-child {
+ border-bottom: none;
+ margin-bottom: 0;
+ }
+
+ .grpc-message-header {
+ display: flex;
+ justify-content: space-between;
+ font-weight: 600;
+ padding: 5px 10px;
+ background-color: rgba(52, 152, 219, 0.05);
+ border-radius: 4px;
+ margin-bottom: 5px;
+ }
+
+ .message-sent .grpc-message-header {
+ background-color: rgba(52, 152, 219, 0.1);
+ }
+
+ .message-received .grpc-message-header {
+ background-color: rgba(46, 204, 113, 0.1);
+ }
+
+ .message-direction {
+ color: var(--primary-color);
+ }
+
+ .message-sent .message-direction {
+ color: var(--primary-color);
+ }
+
+ .message-received .message-direction {
+ color: var(--secondary-color);
+ }
+
+ .grpc-message-content {
+ margin: 0;
+ padding: 10px;
+ max-height: 200px;
+ overflow-y: auto;
+ }
+
+ /* Error text */
+ .error-text {
+ color: var(--danger-color);
+ background-color: rgba(231, 76, 60, 0.05);
+ border-left: 3px solid var(--danger-color);
+ padding-left: 10px;
+ }
+ `;
+ document.head.appendChild(styleElement);
+ }
+
+ function initializeGRPCSupport() {
+ addGRPCStyles();
+
+ // Add type filter to the filters section
+ const methodFilterGroup = document.querySelector(".filter-group:first-child");
+ if (methodFilterGroup) {
+ const typeFilterGroup = document.createElement("div");
+ typeFilterGroup.className = "filter-group";
+ typeFilterGroup.innerHTML = `
+ Request Type
+
+ All Types
+ HTTP
+ gRPC
+
+ `;
+
+ methodFilterGroup.parentNode.insertBefore(typeFilterGroup, methodFilterGroup);
+ }
+
+ // Update method filter to include gRPC method types
+ const methodFilter = document.getElementById("method-filter");
+ if (methodFilter) {
+ // Save current value
+ const currentValue = methodFilter.value;
+
+ // Clear options
+ methodFilter.innerHTML = "";
+
+ // Add HTTP methods
+ const httpOptgroup = document.createElement("optgroup");
+ httpOptgroup.label = "HTTP Methods";
+
+ const httpOptions = [
+ { value: "", label: "All Methods" },
+ { value: "GET", label: "GET" },
+ { value: "POST", label: "POST" },
+ { value: "PUT", label: "PUT" },
+ { value: "DELETE", label: "DELETE" },
+ { value: "PATCH", label: "PATCH" }
+ ];
+
+ httpOptions.forEach(opt => {
+ const option = document.createElement("option");
+ option.value = opt.value;
+ option.textContent = opt.label;
+ if (opt.value === currentValue) {
+ option.selected = true;
+ }
+ httpOptgroup.appendChild(option);
+ });
+
+ methodFilter.appendChild(httpOptgroup);
+
+ // Add gRPC method types
+ const grpcOptgroup = document.createElement("optgroup");
+ grpcOptgroup.label = "gRPC Method Types";
+
+ const grpcOptions = [
+ { value: "unary", label: "Unary" },
+ { value: "client_stream", label: "Client Streaming" },
+ { value: "server_stream", label: "Server Streaming" },
+ { value: "bidi_stream", label: "Bidirectional Streaming" }
+ ];
+
+ grpcOptions.forEach(opt => {
+ const option = document.createElement("option");
+ option.value = opt.value;
+ option.textContent = opt.label;
+ grpcOptgroup.appendChild(option);
+ });
+
+ methodFilter.appendChild(grpcOptgroup);
+ }
+
+ // Update status filter to include gRPC statuses
+ const statusFilter = document.getElementById("status-filter");
+ if (statusFilter) {
+ // Save current value
+ const currentValue = statusFilter.value;
+
+ // Clear options
+ statusFilter.innerHTML = "";
+
+ // Add HTTP status codes
+ const httpOptgroup = document.createElement("optgroup");
+ httpOptgroup.label = "HTTP Status Codes";
+
+ const httpOptions = [
+ { value: "", label: "All Status Codes" },
+ { value: "2xx", label: "2xx Success" },
+ { value: "3xx", label: "3xx Redirect" },
+ { value: "4xx", label: "4xx Client Error" },
+ { value: "5xx", label: "5xx Server Error" }
+ ];
+
+ httpOptions.forEach(opt => {
+ const option = document.createElement("option");
+ option.value = opt.value;
+ option.textContent = opt.label;
+ if (opt.value === currentValue) {
+ option.selected = true;
+ }
+ httpOptgroup.appendChild(option);
+ });
+
+ statusFilter.appendChild(httpOptgroup);
+
+ // Add gRPC status codes
+ const grpcOptgroup = document.createElement("optgroup");
+ grpcOptgroup.label = "gRPC Status Codes";
+
+ const grpcOptions = [
+ { value: "success", label: "OK (0)" },
+ { value: "error", label: "Any Error" },
+ { value: "code-1", label: "Cancelled (1)" },
+ { value: "code-2", label: "Unknown (2)" },
+ { value: "code-3", label: "Invalid Argument (3)" },
+ { value: "code-4", label: "Deadline Exceeded (4)" },
+ { value: "code-5", label: "Not Found (5)" },
+ { value: "code-6", label: "Already Exists (6)" },
+ { value: "code-7", label: "Permission Denied (7)" },
+ { value: "code-13", label: "Internal (13)" },
+ { value: "code-14", label: "Unavailable (14)" }
+ ];
+
+ grpcOptions.forEach(opt => {
+ const option = document.createElement("option");
+ option.value = opt.value;
+ option.textContent = opt.label;
+ grpcOptgroup.appendChild(option);
+ });
+
+ statusFilter.appendChild(grpcOptgroup);
+ }
+
+ // Update table headers to add Type column
+ const requestsTable = document.getElementById("requests");
+ if (requestsTable) {
+ const headerRow = requestsTable.querySelector("thead tr");
+ if (headerRow) {
+ // Look for the Time header
+ const timeHeader = headerRow.querySelector("th:first-child");
+ if (timeHeader) {
+ // Insert Type header after Time
+ const typeHeader = document.createElement("th");
+ typeHeader.textContent = "Type";
+ timeHeader.insertAdjacentElement("afterend", typeHeader);
+ }
+ }
+ }
+
+ // Add gRPC stats to stats section
+ const statsGrid = document.querySelector(".stats-grid");
+ if (statsGrid) {
+ // Create gRPC stat boxes
+ const grpcTotalBox = document.createElement("div");
+ grpcTotalBox.className = "stat-box";
+ grpcTotalBox.innerHTML = `
+ 0
+ gRPC Total
+ `;
+
+ const grpcSuccessBox = document.createElement("div");
+ grpcSuccessBox.className = "stat-box";
+ grpcSuccessBox.innerHTML = `
+ 0
+ gRPC Success
+ `;
+
+ const grpcErrorBox = document.createElement("div");
+ grpcErrorBox.className = "stat-box";
+ grpcErrorBox.innerHTML = `
+ 0
+ gRPC Errors
+ `;
+
+ // Append to stats grid
+ statsGrid.appendChild(grpcTotalBox);
+ statsGrid.appendChild(grpcSuccessBox);
+ statsGrid.appendChild(grpcErrorBox);
+ }
+
+ // Create the gRPC details container
+ createGRPCDetailsContainer();
}
// Show request details
@@ -492,16 +1092,14 @@
const routePattern = document.createElement("div");
routePattern.className = "trace-detail-item";
- routePattern.innerHTML = `Pattern: ${
- req.RouteTrace.pattern || "Unknown"
- }`;
+ routePattern.innerHTML = `Pattern: ${req.RouteTrace.pattern || "Unknown"
+ }`;
routeDetails.appendChild(routePattern);
const routePath = document.createElement("div");
routePath.className = "trace-detail-item";
- routePath.innerHTML = `Path: ${
- req.RouteTrace.path || "Unknown"
- }`;
+ routePath.innerHTML = `Path: ${req.RouteTrace.path || "Unknown"
+ }`;
routeDetails.appendChild(routePath);
if (req.RouteTrace.params) {
@@ -1363,11 +1961,11 @@
row.innerHTML = `
+ name
+ )}" placeholder="Header name">
+ value
+ )}" placeholder="Header value">
`;
@@ -1492,9 +2090,8 @@
let output = "";
for (const [key, values] of Object.entries(headers)) {
- output += `${key}: ${
- Array.isArray(values) ? values.join(", ") : values
- }\n`;
+ output += `${key}: ${Array.isArray(values) ? values.join(", ") : values
+ }\n`;
}
return output;
@@ -1557,5 +2154,7 @@
document.getElementById("replayModal").classList.remove("active");
}
});
+
+ document.addEventListener("DOMContentLoaded", initializeGRPCSupport);
-{{end}}
+{{end}}
\ No newline at end of file
diff --git a/internal/dashboard/templates/layout.html b/internal/dashboard/templates/layout.html
index 451ce05..cc76387 100644
--- a/internal/dashboard/templates/layout.html
+++ b/internal/dashboard/templates/layout.html
@@ -1,1206 +1,1280 @@
-
- {{template "title" .}}
-
+ {{block "head" .}}{{end}}
+
+
+
+ {{template "sidebar" .}}
+
+
+
+
+ {{template "stats" .}} {{template "filters" .}} {{template
+ "request-table" .}}
+
- .replay-status-success {
- background-color: rgba(46, 204, 113, 0.2);
- color: var(--success-color);
- }
+
+
+ {{template "request-details" .}} {{template "trace" .}}
+
- .replay-status-error {
- background-color: rgba(231, 76, 60, 0.2);
- color: var(--danger-color);
- }
+
+
+ {{template "env-info" .}}
+
+
+
+ {{block "content" .}}{{end}}
+
+
+
+ +
+
+
+
+
+
+
+
+
+ Select 2 or more requests to compare
+
+
+
+
+
+
+
+
+