Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,15 @@ docker compose logs -f picoclaw-gateway
docker compose --profile gateway down
```

### Tracing (Jaeger)

```bash
# Start Jaeger alongside the gateway
docker compose --profile gateway --profile tracing up -d

# Jaeger UI: http://localhost:16686
```

### Agent Mode (One-shot)

```bash
Expand Down Expand Up @@ -647,6 +656,49 @@ The subagent has access to tools (message, web_search, etc.) and can communicate
* `PICOCLAW_HEARTBEAT_ENABLED=false` to disable
* `PICOCLAW_HEARTBEAT_INTERVAL=60` to change interval

### Tracing (Jaeger / OpenTelemetry)

PicoClaw supports distributed tracing via OpenTelemetry with Jaeger as the backend. This gives visibility into LLM calls, tool executions, and message processing.

**1. Start Jaeger**

```bash
docker compose --profile tracing up jaeger -d
```

The Jaeger UI will be available at [http://localhost:16686](http://localhost:16686).

**2. Enable tracing** in `~/.picoclaw/config.json`:

```json
{
"tracing": {
"enabled": true,
"endpoint": "localhost:4317"
}
}
```

**3. Run PicoClaw** normally — spans will appear in Jaeger under the `picoclaw-gateway` or `picoclaw-agent` service.

| Option | Default | Description |
|--------|---------|-------------|
| `enabled` | `false` | Enable/disable tracing |
| `endpoint` | `localhost:4317` | OTLP gRPC collector endpoint |

**Environment variables:**
- `PICOCLAW_TRACING_ENABLED=true` to enable
- `PICOCLAW_TRACING_ENDPOINT=localhost:4317` to change endpoint

**Instrumented spans:**

| Span | Description |
|------|-------------|
| `agent.processMessage` | Full message processing (session, channel, chat_id) |
| `agent.llm.call` | Each LLM API call (model, iteration, message count) |
| `agent.tool.execute` | Each tool execution (tool name) |
| `provider.chat` | HTTP provider chat call (model, message count) |

### Providers

> [!NOTE]
Expand Down Expand Up @@ -762,6 +814,10 @@ picoclaw agent -m "Hello"
"heartbeat": {
"enabled": true,
"interval": 30
},
"tracing": {
"enabled": false,
"endpoint": "localhost:4317"
}
}
```
Expand Down
89 changes: 89 additions & 0 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
openapi: "3.1.0"
info:
title: PicoClaw API
version: "1.0.0"
description: REST API for PicoClaw AI Gateway
servers:
- url: http://localhost:18790
security:
- ApiKeyAuth: []
components:
securitySchemes:
ApiKeyAuth:
type: http
scheme: bearer
schemas:
ChatRequest:
type: object
required: [message]
properties:
session_id: { type: string, description: "Session ID (auto-generated if omitted)" }
message: { type: string, description: "User message" }
ChatResponse:
type: object
properties:
session_id: { type: string }
response: { type: string }
SessionList:
type: object
properties:
sessions: { type: array, items: { type: string } }
HealthResponse:
type: object
properties:
status: { type: string, enum: [ok] }
version: { type: string }
Error:
type: object
properties:
error: { type: string }
code: { type: integer }
paths:
/api/chat:
post:
operationId: sendMessage
summary: Send a message and get an AI response
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/ChatRequest" }
responses:
"200":
description: Successful response
content:
application/json:
schema: { $ref: "#/components/schemas/ChatResponse" }
"401": { description: Unauthorized }
"500": { description: Internal error }
/api/sessions:
get:
operationId: listSessions
summary: List active sessions
responses:
"200":
description: Successful response
content:
application/json:
schema: { $ref: "#/components/schemas/SessionList" }
/api/health:
get:
operationId: healthCheck
summary: Health check
security: []
responses:
"200":
description: Successful response
content:
application/json:
schema: { $ref: "#/components/schemas/HealthResponse" }
/api/openapi.yaml:
get:
operationId: getSpec
summary: Serve the OpenAPI specification
security: []
responses:
"200":
description: OpenAPI spec
content:
application/x-yaml: {}
89 changes: 89 additions & 0 deletions cmd/picoclaw/api/openapi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
openapi: "3.1.0"
info:
title: PicoClaw API
version: "1.0.0"
description: REST API for PicoClaw AI Gateway
servers:
- url: http://localhost:18790
security:
- ApiKeyAuth: []
components:
securitySchemes:
ApiKeyAuth:
type: http
scheme: bearer
schemas:
ChatRequest:
type: object
required: [message]
properties:
session_id: { type: string, description: "Session ID (auto-generated if omitted)" }
message: { type: string, description: "User message" }
ChatResponse:
type: object
properties:
session_id: { type: string }
response: { type: string }
SessionList:
type: object
properties:
sessions: { type: array, items: { type: string } }
HealthResponse:
type: object
properties:
status: { type: string, enum: [ok] }
version: { type: string }
Error:
type: object
properties:
error: { type: string }
code: { type: integer }
paths:
/api/chat:
post:
operationId: sendMessage
summary: Send a message and get an AI response
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/ChatRequest" }
responses:
"200":
description: Successful response
content:
application/json:
schema: { $ref: "#/components/schemas/ChatResponse" }
"401": { description: Unauthorized }
"500": { description: Internal error }
/api/sessions:
get:
operationId: listSessions
summary: List active sessions
responses:
"200":
description: Successful response
content:
application/json:
schema: { $ref: "#/components/schemas/SessionList" }
/api/health:
get:
operationId: healthCheck
summary: Health check
security: []
responses:
"200":
description: Successful response
content:
application/json:
schema: { $ref: "#/components/schemas/HealthResponse" }
/api/openapi.yaml:
get:
operationId: getSpec
summary: Serve the OpenAPI specification
security: []
responses:
"200":
description: OpenAPI spec
content:
application/x-yaml: {}
52 changes: 52 additions & 0 deletions cmd/picoclaw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"fmt"
"io"
"io/fs"
"net/http"
"os"
"os/signal"
"path/filepath"
Expand All @@ -28,20 +29,26 @@ import (
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/cron"
"github.com/sipeed/picoclaw/pkg/devices"
"github.com/sipeed/picoclaw/pkg/gateway"
"github.com/sipeed/picoclaw/pkg/heartbeat"
"github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/migrate"
"github.com/sipeed/picoclaw/pkg/providers"
"github.com/sipeed/picoclaw/pkg/skills"
"github.com/sipeed/picoclaw/pkg/state"
"github.com/sipeed/picoclaw/pkg/tools"
"github.com/sipeed/picoclaw/pkg/tracing"
"github.com/sipeed/picoclaw/pkg/voice"
)

//go:generate cp -r ../../workspace .
//go:embed workspace
var embeddedFiles embed.FS

//go:generate cp -r ../../api .
//go:embed api/openapi.yaml
var openapiSpec []byte

var (
version = "dev"
gitCommit string
Expand Down Expand Up @@ -397,6 +404,16 @@ func agentCmd() {
os.Exit(1)
}

// Initialize tracing if enabled
if cfg.Tracing.Enabled {
_, err := tracing.Init("picoclaw-agent", cfg.Tracing.Endpoint)
if err != nil {
logger.WarnCF("agent", "Failed to initialize tracing: %v", map[string]interface{}{"error": err.Error()})
} else {
defer tracing.Shutdown(context.Background())
}
}

provider, err := providers.CreateProvider(cfg)
if err != nil {
fmt.Printf("Error creating provider: %v\n", err)
Expand Down Expand Up @@ -532,6 +549,17 @@ func gatewayCmd() {
os.Exit(1)
}

// Initialize tracing if enabled
if cfg.Tracing.Enabled {
_, err := tracing.Init("picoclaw-gateway", cfg.Tracing.Endpoint)
if err != nil {
logger.WarnCF("gateway", "Failed to initialize tracing: %v", map[string]interface{}{"error": err.Error()})
} else {
defer tracing.Shutdown(context.Background())
fmt.Println("📡 Tracing enabled →", cfg.Tracing.Endpoint)
}
}

provider, err := providers.CreateProvider(cfg)
if err != nil {
fmt.Printf("Error creating provider: %v\n", err)
Expand Down Expand Up @@ -658,6 +686,30 @@ func gatewayCmd() {
fmt.Printf("Error starting channels: %v\n", err)
}

// Start REST API if enabled
if cfg.API.Enabled {
gateway.SetVersion(version)
gateway.SetOpenAPISpec(openapiSpec)
apiServer := gateway.NewAPIServer(agentLoop, cfg.API)
apiAddr := fmt.Sprintf("%s:%d", cfg.Gateway.Host, cfg.Gateway.Port)
httpServer := &http.Server{
Addr: apiAddr,
Handler: apiServer.Handler(),
}
go func() {
fmt.Printf("✓ REST API listening on %s\n", apiAddr)
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.ErrorCF("api", "REST API server error", map[string]interface{}{
"error": err.Error(),
})
}
}()
go func() {
<-ctx.Done()
httpServer.Shutdown(context.Background())
}()
}

go agentLoop.Run(ctx)

sigChan := make(chan os.Signal, 1)
Expand Down
15 changes: 15 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,20 @@ services:
- picoclaw-workspace:/root/.picoclaw/workspace
command: ["gateway"]

# ─────────────────────────────────────────────
# Jaeger (Tracing UI + OTLP collector)
# UI: http://localhost:16686
# ─────────────────────────────────────────────
jaeger:
image: jaegertracing/jaeger:2
container_name: picoclaw-jaeger
profiles:
- tracing
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPC
environment:
- COLLECTOR_OTLP_ENABLED=true

volumes:
picoclaw-workspace:
Loading