chore: import upstream snapshot with attribution
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
# Examples wayfinding
|
||||
|
||||
Use this index when you want the shortest path from a first runnable agent to the
|
||||
next services, agents, workflows, and interop examples. Every command below is
|
||||
provider-free unless the example README says otherwise.
|
||||
|
||||
## Pick by goal
|
||||
|
||||
| Goal | Start here | Run or verify | Then try |
|
||||
|------|------------|---------------|----------|
|
||||
| Run the smallest no-secret agent | [`first-agent`](./first-agent/) | `go run ./examples/first-agent` | [`agent-demo`](./agent-demo/) for a larger service-backed agent |
|
||||
| Prove the maintained 0→hero path | [`support`](./support/) | `go run ./examples/support` and `go test ./examples/support` | [`zero-to-hero` guide](../internal/website/docs/guides/zero-to-hero.md) |
|
||||
| See planning and delegation | [`agent-plan-delegate`](./agent-plan-delegate/) | `go run ./examples/agent-plan-delegate` | [`plan-delegate` guide](../internal/website/docs/guides/plan-delegate.md) |
|
||||
| Expose services through MCP | [`mcp/hello`](./mcp/hello/) | follow [`mcp`](./mcp/) setup | [`mcp/crud`](./mcp/crud/) and [`mcp/workflow`](./mcp/workflow/) |
|
||||
| Try a paid tool with x402 | [`agent-x402-buyer`](./agent-x402-buyer/) | `go run ./examples/agent-x402-buyer` | [`Payments (x402)` guide](../internal/website/docs/guides/x402-payments.md) |
|
||||
| Try A2A or gRPC interop next | [`agent-demo`](./agent-demo/) plus gateway docs | run the example, then use the gateway docs | [`grpc-interop`](./grpc-interop/) |
|
||||
| Add workflow durability | [`flow-durable`](./flow-durable/) | `go run ./examples/flow-durable` | [`flow-loop`](./flow-loop/) |
|
||||
|
||||
## Recommended adoption path
|
||||
|
||||
1. **First service:** run [`hello-world`](./hello-world/) to learn service
|
||||
registration, handlers, client calls, and health checks.
|
||||
2. **First agent:** run [`first-agent`](./first-agent/) with
|
||||
`go run ./examples/first-agent`; it uses a deterministic mock model and needs
|
||||
no provider key.
|
||||
3. **0→hero reference:** run [`support`](./support/) with
|
||||
`go run ./examples/support`; it keeps typed services, an agent chat loop, an
|
||||
event-driven flow, and an approval gate in one maintained example.
|
||||
4. **Interop next:** use [`mcp/hello`](./mcp/hello/), [`mcp/crud`](./mcp/crud/),
|
||||
and [`mcp/workflow`](./mcp/workflow/) when you are ready to expose tools to
|
||||
external AI clients.
|
||||
5. **Paid tools:** run [`agent-x402-buyer`](./agent-x402-buyer/) to see an
|
||||
agent pay a local x402-protected tool with a mock facilitator and budget.
|
||||
6. **Workflow depth:** use [`flow-durable`](./flow-durable/) once the agent path
|
||||
needs checkpointed, resumable deterministic work.
|
||||
|
||||
## CLI wayfinding
|
||||
|
||||
The installed CLI prints the same path:
|
||||
|
||||
```bash
|
||||
micro examples
|
||||
micro agent demo
|
||||
micro zero-to-hero
|
||||
```
|
||||
|
||||
Keep this file, [`README.md`](../README.md), and the `micro examples` output in
|
||||
sync so new developers can find `examples/first-agent` and `examples/support`
|
||||
from one documented path.
|
||||
@@ -0,0 +1,167 @@
|
||||
# Go Micro Examples
|
||||
|
||||
This directory contains runnable examples that take you through the Go Micro
|
||||
lifecycle: start with a service, expose it as agent-usable capability, then
|
||||
coordinate work with workflows.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Each example can be run with `go run .` from its directory unless its README says
|
||||
otherwise. If you are new to the repo, start with the [examples wayfinding index](./INDEX.md)
|
||||
or follow the first-agent path below instead of reading the directories alphabetically.
|
||||
|
||||
## Recommended first-agent path
|
||||
|
||||
This path is the canonical services → agents → workflows route through the examples map. Debugging and observability wayfinding stays nearby once the first run works.
|
||||
|
||||
| Step | Start here | What you learn | Next step |
|
||||
|------|------------|----------------|-----------|
|
||||
| 1. First service | [`hello-world`](./hello-world/) | Build the 0→1 service path: create and register a basic RPC service, add a handler, call it with a client, and expose health checks. | Move to [`agent-demo`](./agent-demo/) to see services used by an agent. |
|
||||
| 2. First agent | [`first-agent`](./first-agent/) | Run the smallest service-backed agent with a deterministic mock model and no provider key. | Compare with [`agent-demo`](./agent-demo/) or the maintained 0-to-hero path in [`support`](./support/). |
|
||||
| 3. First workflow | [`support`](./support/) | Follow typed services into an agent chat loop, an event-driven `intake` flow, and an approval gate in one runnable reference. | Deepen the workflow model with [`flow-durable`](./flow-durable/). |
|
||||
|
||||
For the shortest AI-tooling bridge, the MCP path is
|
||||
[`mcp/hello`](./mcp/hello/) → [`mcp/crud`](./mcp/crud/) →
|
||||
[`mcp/workflow`](./mcp/workflow/). For debugging and production hardening, keep
|
||||
[`agent-wrap-tool`](./agent-wrap-tool/), [`agent-durable`](./agent-durable/), and
|
||||
[`deployment`](./deployment/) nearby.
|
||||
|
||||
## Lifecycle map
|
||||
|
||||
### 1. Services — learn the runtime foundation
|
||||
|
||||
#### [hello-world](./hello-world/)
|
||||
Basic RPC service demonstrating core concepts:
|
||||
- Service creation and registration
|
||||
- Handler implementation
|
||||
- Client calls
|
||||
- Health checks
|
||||
|
||||
**Run it:**
|
||||
```bash
|
||||
cd hello-world
|
||||
go run .
|
||||
```
|
||||
|
||||
#### [web-service](./web-service/)
|
||||
HTTP web service with service discovery:
|
||||
- HTTP handlers
|
||||
- Service registration
|
||||
- Health checks
|
||||
- JSON REST API
|
||||
|
||||
**Run it:**
|
||||
```bash
|
||||
cd web-service
|
||||
go run .
|
||||
```
|
||||
|
||||
#### [multi-service](./multi-service/)
|
||||
Multiple services in a single binary — the modular monolith pattern:
|
||||
- Isolated server, client, store, and cache per service
|
||||
- Shared registry and broker for inter-service communication
|
||||
- Coordinated lifecycle with `service.Group`
|
||||
- Start monolith, split later when you need to scale independently
|
||||
|
||||
**Run it:**
|
||||
```bash
|
||||
cd multi-service
|
||||
go run .
|
||||
```
|
||||
|
||||
#### [deployment](./deployment/)
|
||||
Docker Compose deployment with MCP gateway, Consul registry, and Jaeger tracing:
|
||||
- Production-like architecture in one `docker-compose up`
|
||||
- Standalone MCP gateway connected to service registry
|
||||
- Distributed tracing with OpenTelemetry + Jaeger
|
||||
|
||||
### 2. Agents — turn services into tool-using teammates
|
||||
|
||||
#### [first-agent](./first-agent/)
|
||||
Smallest first agent: one notes service plus one scoped agent, backed by a deterministic mock model so `go run ./examples/first-agent` works without provider secrets.
|
||||
|
||||
#### [agent-demo](./agent-demo/)
|
||||
A multi-service project management app with Projects,
|
||||
Tasks, and Team services, seed data, and agent playground integration.
|
||||
|
||||
#### [agent-plan-delegate](./agent-plan-delegate/)
|
||||
The two built-in agent capabilities in a small multi-agent system:
|
||||
- **plan** — an agent records an ordered plan in its store-backed memory before doing multi-step work
|
||||
- **delegate** — an agent hands a subtask to another agent (over RPC if it's registered, else to an ephemeral sub-agent)
|
||||
|
||||
#### [agent-wrap-tool](./agent-wrap-tool/)
|
||||
Middleware around an agent's tool execution with `AgentWrapTool`, the tool-side analogue of client/server wrappers:
|
||||
- **observe** — time every tool call and record per-tool metrics, correlated by call ID
|
||||
- **retry** — re-run a call whose result is an error, recovering from a transient failure before the model sees it
|
||||
|
||||
#### [agent-durable](./agent-durable/)
|
||||
Durable agent runs that can be checkpointed and resumed, useful once your first
|
||||
agent needs predictable recovery behavior.
|
||||
|
||||
#### [agent-human-input](./agent-human-input/)
|
||||
Human-in-the-loop agent interaction for decisions that need an explicit person
|
||||
before the run can continue.
|
||||
|
||||
#### [agent-ollama](./agent-ollama/)
|
||||
Local-model agent wiring for developers experimenting with Ollama-backed model
|
||||
calls.
|
||||
|
||||
### 3. Workflows — coordinate longer-running work
|
||||
|
||||
#### [support](./support/)
|
||||
A maintained 0-to-hero reference path in one runnable file:
|
||||
- **scaffold** typed `customers`, `tickets`, and `notify` services
|
||||
- **run/chat** with a support agent that uses those services as tools
|
||||
- **inspect** the event-driven `intake` flow and approval gate
|
||||
- **CI** keeps the deterministic mock-model journey runnable with `go test ./examples/support`
|
||||
|
||||
#### [flow-durable](./flow-durable/)
|
||||
A workflow as ordered, checkpointed steps that survives a crash and resumes where it stopped:
|
||||
- **steps** — a flow is a task with stages (`reserve → charge → confirm`), not just one LLM turn
|
||||
- **Checkpoint** — each step is persisted; on `Resume`, completed steps are not re-run (no duplicate side effects)
|
||||
|
||||
#### [flow-loop](./flow-loop/)
|
||||
A looping flow example for repeated workflow steps.
|
||||
|
||||
### 4. MCP and agent integration examples
|
||||
|
||||
See the [mcp/](./mcp/) directory for AI agent integration examples:
|
||||
- **[hello](./mcp/hello/)** - Minimal MCP service (start here)
|
||||
- **[crud](./mcp/crud/)** - CRUD contact book with full agent documentation
|
||||
- **[workflow](./mcp/workflow/)** - Cross-service orchestration via AI agents
|
||||
- **[documented](./mcp/documented/)** - All MCP features with auth scopes
|
||||
- **[platform](./mcp/platform/)** - Platform-oriented MCP service example
|
||||
|
||||
## Other examples
|
||||
|
||||
### [auth](./auth/)
|
||||
Authentication and authorization example.
|
||||
|
||||
### [graceful-stop](./graceful-stop/)
|
||||
Graceful shutdown behavior for long-running services.
|
||||
|
||||
### [grpc-interop](./grpc-interop/)
|
||||
gRPC interoperability example.
|
||||
|
||||
## Coming Soon
|
||||
|
||||
- **pubsub-events** - Event-driven architecture with NATS
|
||||
- **grpc-integration** - Using go-micro with gRPC
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Some examples require external dependencies:
|
||||
|
||||
- **NATS**: `docker run -p 4222:4222 nats:latest`
|
||||
- **Consul**: `docker run -p 8500:8500 consul:latest agent -dev -ui -client=0.0.0.0`
|
||||
- **Redis**: `docker run -p 6379:6379 redis:latest`
|
||||
|
||||
## Contributing
|
||||
|
||||
To add a new example:
|
||||
|
||||
1. Create a new directory
|
||||
2. Add a descriptive README.md
|
||||
3. Include working code with comments
|
||||
4. Add to this index under the lifecycle stage it supports
|
||||
5. Ensure it runs with `go run .`
|
||||
@@ -0,0 +1,63 @@
|
||||
# Agent Demo
|
||||
|
||||
A multi-service project management app that demonstrates AI agents interacting with Go Micro services through MCP.
|
||||
|
||||
## What's Included
|
||||
|
||||
Three services registered in a single process:
|
||||
|
||||
| Service | Endpoints | Description |
|
||||
|---------|-----------|-------------|
|
||||
| **ProjectService** | Create, Get, List | Manage projects with status tracking |
|
||||
| **TaskService** | Create, List, Update | Tasks with assignees, priorities, and status |
|
||||
| **TeamService** | Add, List, Get | Team members with roles and skills |
|
||||
|
||||
The demo starts with seed data: 2 projects, 7 tasks, and 4 team members.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
go run main.go
|
||||
```
|
||||
|
||||
Endpoints:
|
||||
- **MCP Gateway:** http://localhost:3000
|
||||
- **MCP Tools:** http://localhost:3000/mcp/tools
|
||||
- **WebSocket:** ws://localhost:3000/mcp/ws
|
||||
|
||||
## Use with Claude Code
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"demo": {
|
||||
"command": "go",
|
||||
"args": ["run", "main.go"],
|
||||
"cwd": "examples/agent-demo"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example Prompts
|
||||
|
||||
Try these with Claude Code or any MCP client:
|
||||
|
||||
- "What projects do we have?"
|
||||
- "Show me all tasks assigned to alice"
|
||||
- "Create a high-priority task for bob to review the design mockups"
|
||||
- "Who on the team knows Go?"
|
||||
- "Give me a status update on the Website Redesign project"
|
||||
- "What tasks are still todo on the API v2 migration?"
|
||||
- "Assign the unassigned tasks to charlie"
|
||||
- "Mark task-1 as done"
|
||||
|
||||
## What This Demonstrates
|
||||
|
||||
1. **Zero-config MCP** — Services become AI tools automatically from doc comments
|
||||
2. **Cross-service orchestration** — An agent queries projects, tasks, and team in one conversation
|
||||
3. **Rich tool descriptions** — `description` struct tags and `@example` comments guide the agent
|
||||
4. **Auth scopes** — Read and write operations have separate scopes
|
||||
5. **`WithMCP` one-liner** — MCP gateway starts with a single option
|
||||
|
||||
See the [blog post](/blog/4) for a detailed walkthrough.
|
||||
@@ -0,0 +1,454 @@
|
||||
// Agent Demo — A multi-service project management app
|
||||
//
|
||||
// This example shows three Go Micro services (projects, tasks, team)
|
||||
// working together through the MCP gateway, letting an AI agent
|
||||
// manage projects using natural language.
|
||||
//
|
||||
// Run:
|
||||
//
|
||||
// go run main.go
|
||||
//
|
||||
// Then open the agent playground at http://localhost:8080/agent
|
||||
// or connect Claude Code via: micro mcp serve
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
"go-micro.dev/v6/gateway/mcp"
|
||||
"go-micro.dev/v6/server"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Projects service
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type Project struct {
|
||||
ID string `json:"id" description:"Unique project identifier"`
|
||||
Name string `json:"name" description:"Project name"`
|
||||
Description string `json:"description" description:"What the project is about"`
|
||||
Status string `json:"status" description:"Project status: planning, active, or completed"`
|
||||
CreatedAt time.Time `json:"created_at" description:"When the project was created"`
|
||||
}
|
||||
|
||||
type CreateProjectRequest struct {
|
||||
Name string `json:"name" description:"Project name (required)"`
|
||||
Description string `json:"description" description:"Short description of the project"`
|
||||
}
|
||||
|
||||
type CreateProjectResponse struct {
|
||||
Project *Project `json:"project" description:"The newly created project"`
|
||||
}
|
||||
|
||||
type GetProjectRequest struct {
|
||||
ID string `json:"id" description:"Project ID to retrieve"`
|
||||
}
|
||||
|
||||
type GetProjectResponse struct {
|
||||
Project *Project `json:"project" description:"The requested project"`
|
||||
}
|
||||
|
||||
type ListProjectsRequest struct {
|
||||
Status string `json:"status,omitempty" description:"Filter by status: planning, active, completed (optional)"`
|
||||
}
|
||||
|
||||
type ListProjectsResponse struct {
|
||||
Projects []*Project `json:"projects" description:"List of matching projects"`
|
||||
}
|
||||
|
||||
type ProjectService struct {
|
||||
mu sync.RWMutex
|
||||
projects map[string]*Project
|
||||
nextID int
|
||||
}
|
||||
|
||||
// Create creates a new project with the given name and description.
|
||||
// Returns the project with a generated ID and initial status of "planning".
|
||||
//
|
||||
// @example {"name": "Website Redesign", "description": "Redesign the company website with new branding"}
|
||||
func (s *ProjectService) Create(ctx context.Context, req *CreateProjectRequest, rsp *CreateProjectResponse) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.nextID++
|
||||
p := &Project{
|
||||
ID: fmt.Sprintf("proj-%d", s.nextID),
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Status: "planning",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
s.projects[p.ID] = p
|
||||
rsp.Project = p
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get retrieves a project by ID.
|
||||
// Returns an error if the project does not exist.
|
||||
//
|
||||
// @example {"id": "proj-1"}
|
||||
func (s *ProjectService) Get(ctx context.Context, req *GetProjectRequest, rsp *GetProjectResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
p, ok := s.projects[req.ID]
|
||||
if !ok {
|
||||
return fmt.Errorf("project %s not found", req.ID)
|
||||
}
|
||||
rsp.Project = p
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns all projects, optionally filtered by status.
|
||||
// Valid status values: planning, active, completed.
|
||||
//
|
||||
// @example {"status": "active"}
|
||||
func (s *ProjectService) List(ctx context.Context, req *ListProjectsRequest, rsp *ListProjectsResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, p := range s.projects {
|
||||
if req.Status == "" || p.Status == req.Status {
|
||||
rsp.Projects = append(rsp.Projects, p)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tasks service
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type Task struct {
|
||||
ID string `json:"id" description:"Unique task identifier"`
|
||||
ProjectID string `json:"project_id" description:"ID of the project this task belongs to"`
|
||||
Title string `json:"title" description:"Short task title"`
|
||||
Status string `json:"status" description:"Task status: todo, in_progress, or done"`
|
||||
Assignee string `json:"assignee,omitempty" description:"Username of the person assigned"`
|
||||
Priority string `json:"priority" description:"Priority: low, medium, or high"`
|
||||
}
|
||||
|
||||
type CreateTaskRequest struct {
|
||||
ProjectID string `json:"project_id" description:"Project ID to add the task to (required)"`
|
||||
Title string `json:"title" description:"Task title (required)"`
|
||||
Assignee string `json:"assignee,omitempty" description:"Username to assign (optional)"`
|
||||
Priority string `json:"priority,omitempty" description:"Priority: low, medium, or high (default: medium)"`
|
||||
}
|
||||
|
||||
type CreateTaskResponse struct {
|
||||
Task *Task `json:"task" description:"The newly created task"`
|
||||
}
|
||||
|
||||
type ListTasksRequest struct {
|
||||
ProjectID string `json:"project_id,omitempty" description:"Filter by project ID (optional)"`
|
||||
Assignee string `json:"assignee,omitempty" description:"Filter by assignee username (optional)"`
|
||||
Status string `json:"status,omitempty" description:"Filter by status: todo, in_progress, done (optional)"`
|
||||
}
|
||||
|
||||
type ListTasksResponse struct {
|
||||
Tasks []*Task `json:"tasks" description:"List of matching tasks"`
|
||||
}
|
||||
|
||||
type UpdateTaskRequest struct {
|
||||
ID string `json:"id" description:"Task ID to update"`
|
||||
Status string `json:"status,omitempty" description:"New status: todo, in_progress, or done"`
|
||||
Assignee string `json:"assignee,omitempty" description:"New assignee username"`
|
||||
}
|
||||
|
||||
type UpdateTaskResponse struct {
|
||||
Task *Task `json:"task" description:"The updated task"`
|
||||
}
|
||||
|
||||
type TaskService struct {
|
||||
mu sync.RWMutex
|
||||
tasks map[string]*Task
|
||||
nextID int
|
||||
}
|
||||
|
||||
// Create creates a new task in a project.
|
||||
// Returns the task with a generated ID, initial status of "todo", and default priority of "medium".
|
||||
//
|
||||
// @example {"project_id": "proj-1", "title": "Design homepage mockup", "assignee": "alice", "priority": "high"}
|
||||
func (s *TaskService) Create(ctx context.Context, req *CreateTaskRequest, rsp *CreateTaskResponse) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.nextID++
|
||||
priority := req.Priority
|
||||
if priority == "" {
|
||||
priority = "medium"
|
||||
}
|
||||
t := &Task{
|
||||
ID: fmt.Sprintf("task-%d", s.nextID),
|
||||
ProjectID: req.ProjectID,
|
||||
Title: req.Title,
|
||||
Status: "todo",
|
||||
Assignee: req.Assignee,
|
||||
Priority: priority,
|
||||
}
|
||||
s.tasks[t.ID] = t
|
||||
rsp.Task = t
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns tasks filtered by project, assignee, or status.
|
||||
// All filters are optional; omit all to list every task.
|
||||
//
|
||||
// @example {"project_id": "proj-1", "status": "todo"}
|
||||
func (s *TaskService) List(ctx context.Context, req *ListTasksRequest, rsp *ListTasksResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, t := range s.tasks {
|
||||
if req.ProjectID != "" && t.ProjectID != req.ProjectID {
|
||||
continue
|
||||
}
|
||||
if req.Assignee != "" && t.Assignee != req.Assignee {
|
||||
continue
|
||||
}
|
||||
if req.Status != "" && t.Status != req.Status {
|
||||
continue
|
||||
}
|
||||
rsp.Tasks = append(rsp.Tasks, t)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update modifies a task's status or assignee.
|
||||
// Only provided fields are changed; omitted fields stay the same.
|
||||
// Returns an error if the task does not exist.
|
||||
//
|
||||
// @example {"id": "task-1", "status": "in_progress"}
|
||||
func (s *TaskService) Update(ctx context.Context, req *UpdateTaskRequest, rsp *UpdateTaskResponse) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
t, ok := s.tasks[req.ID]
|
||||
if !ok {
|
||||
return fmt.Errorf("task %s not found", req.ID)
|
||||
}
|
||||
if req.Status != "" {
|
||||
t.Status = req.Status
|
||||
}
|
||||
if req.Assignee != "" {
|
||||
t.Assignee = req.Assignee
|
||||
}
|
||||
rsp.Task = t
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Team service
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type Member struct {
|
||||
Username string `json:"username" description:"Unique username"`
|
||||
Name string `json:"name" description:"Display name"`
|
||||
Role string `json:"role" description:"Role: engineer, designer, or manager"`
|
||||
Skills []string `json:"skills" description:"List of skills (e.g. go, react, figma)"`
|
||||
}
|
||||
|
||||
type AddMemberRequest struct {
|
||||
Username string `json:"username" description:"Unique username (required)"`
|
||||
Name string `json:"name" description:"Display name (required)"`
|
||||
Role string `json:"role" description:"Role: engineer, designer, or manager"`
|
||||
Skills []string `json:"skills,omitempty" description:"List of skills"`
|
||||
}
|
||||
|
||||
type AddMemberResponse struct {
|
||||
Member *Member `json:"member" description:"The added team member"`
|
||||
}
|
||||
|
||||
type ListMembersRequest struct {
|
||||
Role string `json:"role,omitempty" description:"Filter by role: engineer, designer, manager (optional)"`
|
||||
Skill string `json:"skill,omitempty" description:"Filter by skill (optional, e.g. 'go' or 'react')"`
|
||||
}
|
||||
|
||||
type ListMembersResponse struct {
|
||||
Members []*Member `json:"members" description:"List of matching team members"`
|
||||
}
|
||||
|
||||
type GetMemberRequest struct {
|
||||
Username string `json:"username" description:"Username to look up"`
|
||||
}
|
||||
|
||||
type GetMemberResponse struct {
|
||||
Member *Member `json:"member" description:"The team member"`
|
||||
}
|
||||
|
||||
type TeamService struct {
|
||||
mu sync.RWMutex
|
||||
members map[string]*Member
|
||||
}
|
||||
|
||||
// Add adds a new team member.
|
||||
// Returns the member with their assigned role and skills.
|
||||
//
|
||||
// @example {"username": "alice", "name": "Alice Chen", "role": "engineer", "skills": ["go", "react"]}
|
||||
func (s *TeamService) Add(ctx context.Context, req *AddMemberRequest, rsp *AddMemberResponse) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
m := &Member{
|
||||
Username: req.Username,
|
||||
Name: req.Name,
|
||||
Role: req.Role,
|
||||
Skills: req.Skills,
|
||||
}
|
||||
s.members[m.Username] = m
|
||||
rsp.Member = m
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns team members, optionally filtered by role or skill.
|
||||
//
|
||||
// @example {"role": "engineer"}
|
||||
func (s *TeamService) List(ctx context.Context, req *ListMembersRequest, rsp *ListMembersResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, m := range s.members {
|
||||
if req.Role != "" && m.Role != req.Role {
|
||||
continue
|
||||
}
|
||||
if req.Skill != "" && !hasSkill(m.Skills, req.Skill) {
|
||||
continue
|
||||
}
|
||||
rsp.Members = append(rsp.Members, m)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get retrieves a team member by username.
|
||||
// Returns an error if the member does not exist.
|
||||
//
|
||||
// @example {"username": "alice"}
|
||||
func (s *TeamService) Get(ctx context.Context, req *GetMemberRequest, rsp *GetMemberResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
m, ok := s.members[req.Username]
|
||||
if !ok {
|
||||
return fmt.Errorf("member %s not found", req.Username)
|
||||
}
|
||||
rsp.Member = m
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasSkill(skills []string, target string) bool {
|
||||
for _, s := range skills {
|
||||
if strings.EqualFold(s, target) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main — wire everything together
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func main() {
|
||||
// Create the service
|
||||
service := micro.NewService("demo",
|
||||
micro.Address(":9090"),
|
||||
// Start MCP gateway alongside the service
|
||||
mcp.WithMCP(":3000"),
|
||||
)
|
||||
service.Init()
|
||||
|
||||
// Register all three handlers with scopes
|
||||
service.Handle(
|
||||
&ProjectService{projects: make(map[string]*Project)},
|
||||
server.WithEndpointScopes("ProjectService.Create", "projects:write"),
|
||||
server.WithEndpointScopes("ProjectService.Get", "projects:read"),
|
||||
server.WithEndpointScopes("ProjectService.List", "projects:read"),
|
||||
)
|
||||
|
||||
service.Handle(
|
||||
&TaskService{tasks: make(map[string]*Task)},
|
||||
server.WithEndpointScopes("TaskService.Create", "tasks:write"),
|
||||
server.WithEndpointScopes("TaskService.List", "tasks:read"),
|
||||
server.WithEndpointScopes("TaskService.Update", "tasks:write"),
|
||||
)
|
||||
|
||||
service.Handle(
|
||||
&TeamService{members: make(map[string]*Member)},
|
||||
server.WithEndpointScopes("TeamService.Add", "team:write"),
|
||||
server.WithEndpointScopes("TeamService.List", "team:read"),
|
||||
server.WithEndpointScopes("TeamService.Get", "team:read"),
|
||||
)
|
||||
|
||||
// Seed some demo data
|
||||
seedData(service.Server())
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println(" Agent Demo")
|
||||
fmt.Println()
|
||||
fmt.Println(" MCP Gateway http://localhost:3000")
|
||||
fmt.Println(" MCP Tools http://localhost:3000/mcp/tools")
|
||||
fmt.Println(" WebSocket ws://localhost:3000/mcp/ws")
|
||||
fmt.Println()
|
||||
fmt.Println(" Try these prompts with Claude Code or the agent playground:")
|
||||
fmt.Println()
|
||||
fmt.Println(" \"What projects do we have?\"")
|
||||
fmt.Println(" \"Create a task for alice to design the new landing page\"")
|
||||
fmt.Println(" \"Show me all high-priority tasks that are still todo\"")
|
||||
fmt.Println(" \"Who on the team knows React?\"")
|
||||
fmt.Println(" \"Give me a status update on the Website Redesign project\"")
|
||||
fmt.Println()
|
||||
|
||||
service.Run()
|
||||
}
|
||||
|
||||
// seedData pre-populates the services with realistic demo data.
|
||||
func seedData(srv server.Server) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Seed team members
|
||||
team := &TeamService{members: make(map[string]*Member)}
|
||||
for _, m := range []AddMemberRequest{
|
||||
{Username: "alice", Name: "Alice Chen", Role: "engineer", Skills: []string{"go", "grpc", "kubernetes"}},
|
||||
{Username: "bob", Name: "Bob Park", Role: "designer", Skills: []string{"figma", "css", "react"}},
|
||||
{Username: "charlie", Name: "Charlie Kim", Role: "engineer", Skills: []string{"go", "react", "postgres"}},
|
||||
{Username: "diana", Name: "Diana Flores", Role: "manager", Skills: []string{"project-management", "scrum"}},
|
||||
} {
|
||||
req := m
|
||||
team.Add(ctx, &req, &AddMemberResponse{})
|
||||
}
|
||||
|
||||
// Seed projects
|
||||
projects := &ProjectService{projects: make(map[string]*Project)}
|
||||
projects.Create(ctx, &CreateProjectRequest{
|
||||
Name: "Website Redesign",
|
||||
Description: "Redesign the company website with new branding and improved UX",
|
||||
}, &CreateProjectResponse{})
|
||||
projects.projects["proj-1"].Status = "active"
|
||||
|
||||
projects.Create(ctx, &CreateProjectRequest{
|
||||
Name: "API v2 Migration",
|
||||
Description: "Migrate all services from REST to gRPC with backward compatibility",
|
||||
}, &CreateProjectResponse{})
|
||||
projects.projects["proj-2"].Status = "planning"
|
||||
|
||||
// Seed tasks
|
||||
tasks := &TaskService{tasks: make(map[string]*Task)}
|
||||
for _, t := range []CreateTaskRequest{
|
||||
{ProjectID: "proj-1", Title: "Design new homepage layout", Assignee: "bob", Priority: "high"},
|
||||
{ProjectID: "proj-1", Title: "Implement responsive nav component", Assignee: "charlie", Priority: "high"},
|
||||
{ProjectID: "proj-1", Title: "Write copy for about page", Priority: "medium"},
|
||||
{ProjectID: "proj-1", Title: "Set up CI/CD for new site", Assignee: "alice", Priority: "medium"},
|
||||
{ProjectID: "proj-2", Title: "Audit existing REST endpoints", Assignee: "alice", Priority: "high"},
|
||||
{ProjectID: "proj-2", Title: "Design gRPC proto files", Priority: "medium"},
|
||||
{ProjectID: "proj-2", Title: "Write migration guide", Assignee: "diana", Priority: "low"},
|
||||
} {
|
||||
req := t
|
||||
tasks.Create(ctx, &req, &CreateTaskResponse{})
|
||||
}
|
||||
// Mark a couple tasks as in_progress
|
||||
tasks.tasks["task-1"].Status = "in_progress"
|
||||
tasks.tasks["task-5"].Status = "in_progress"
|
||||
|
||||
// Register the seeded handlers (replace the empty ones registered above)
|
||||
// Note: in a real app these would be separate services. Here we register
|
||||
// pre-seeded instances so the demo starts with data.
|
||||
srv.Handle(srv.NewHandler(projects))
|
||||
srv.Handle(srv.NewHandler(tasks))
|
||||
srv.Handle(srv.NewHandler(team))
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
# Durable agent run resume
|
||||
|
||||
This example shows the agent-side counterpart to `examples/flow-durable`: an
|
||||
agent run is checkpointed with the same `Checkpoint` interface used by flows,
|
||||
then resumed after an interruption without repeating a completed side effect.
|
||||
The sample uses an in-memory store to keep repeated local runs deterministic;
|
||||
use your service store for process-restart recovery.
|
||||
|
||||
Run it with:
|
||||
|
||||
```sh
|
||||
go run ./examples/agent-durable
|
||||
```
|
||||
|
||||
The demo model calls `inventory.reserve`, then fails to mimic a process dying
|
||||
after the tool call was checkpointed. `micro.AgentPending` finds the unfinished
|
||||
run and `micro.AgentResume` continues it from the saved checkpoint. The final
|
||||
`tool executions: 1` line is the important bit: the reservation tool was not
|
||||
called a second time during resume.
|
||||
|
||||
## When to use this instead of a durable flow
|
||||
|
||||
Use a durable flow when the path is known ahead of time: ordered service calls,
|
||||
retries, timers, compensation, and a precise resume stage such as `reserve` or
|
||||
`charge`. Use a checkpointed agent run when the path is open-ended and the model
|
||||
may choose tools dynamically, but completed tool side effects still must not be
|
||||
replayed after a crash or provider failure.
|
||||
|
||||
They compose: keep deterministic business process in `flow-durable`, then hand
|
||||
off the judgment-heavy step to a checkpointed agent when the workflow needs
|
||||
model-directed tool use. Both use the same `Checkpoint` backend, so inspection
|
||||
and recovery can share one run-history store.
|
||||
|
||||
In a service, use the same pattern at startup:
|
||||
|
||||
```go
|
||||
pending, _ := micro.AgentPending(ctx, agent)
|
||||
for _, run := range pending {
|
||||
_, _ = micro.AgentResume(ctx, agent, run.ID)
|
||||
}
|
||||
```
|
||||
|
||||
`context.Context` cancellation and deadlines are still honored by checkpoint
|
||||
loads/saves, model calls, and tool calls. Runs with terminal statuses such as
|
||||
`done`, `canceled`, and `expired` are not returned by `AgentPending`.
|
||||
@@ -0,0 +1,88 @@
|
||||
// Package main demonstrates durable agent runs: a checkpointed agent can
|
||||
// resume after a crash without re-executing completed tool calls.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
micro "go-micro.dev/v6"
|
||||
"go-micro.dev/v6/ai"
|
||||
"go-micro.dev/v6/store"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
checkpoint := micro.StoreCheckpoint(store.NewMemoryStore(), "durable-agent-demo")
|
||||
model := &demoModel{failFirst: true}
|
||||
ai.Register("durable-demo", func(opts ...ai.Option) ai.Model {
|
||||
_ = model.Init(opts...)
|
||||
return model
|
||||
})
|
||||
var reservations atomic.Int32
|
||||
|
||||
ag := micro.NewAgent("durable-agent-demo",
|
||||
micro.AgentWithCheckpoint(checkpoint),
|
||||
micro.AgentProvider("durable-demo"),
|
||||
micro.AgentTool("inventory.reserve", "reserve inventory exactly once", map[string]any{
|
||||
"sku": map[string]any{"type": "string"},
|
||||
}, func(ctx context.Context, input map[string]any) (string, error) {
|
||||
count := reservations.Add(1)
|
||||
return fmt.Sprintf("reserved %s (execution %d)", input["sku"], count), nil
|
||||
}),
|
||||
)
|
||||
|
||||
_, err := ag.Ask(ctx, "reserve sku-123 and confirm")
|
||||
fmt.Println("initial run:", err)
|
||||
|
||||
pending, err := micro.AgentPending(ctx, ag)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(pending) == 0 {
|
||||
panic("expected a checkpointed run to resume")
|
||||
}
|
||||
|
||||
resp, err := micro.AgentResume(ctx, ag, pending[0].ID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println("resumed reply:", resp.Reply)
|
||||
fmt.Println("tool executions:", reservations.Load())
|
||||
}
|
||||
|
||||
type demoModel struct {
|
||||
failFirst bool
|
||||
opts ai.Options
|
||||
}
|
||||
|
||||
func (m *demoModel) Init(opts ...ai.Option) error {
|
||||
m.opts = ai.NewOptions(opts...)
|
||||
return nil
|
||||
}
|
||||
func (m *demoModel) Options() ai.Options { return m.opts }
|
||||
func (m *demoModel) String() string { return "durable-demo" }
|
||||
|
||||
func (m *demoModel) Generate(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (*ai.Response, error) {
|
||||
if m.opts.ToolHandler != nil {
|
||||
res := m.opts.ToolHandler(ctx, ai.ToolCall{
|
||||
ID: "reserve-1",
|
||||
Name: "inventory.reserve",
|
||||
Input: map[string]any{"sku": "sku-123"},
|
||||
})
|
||||
if res.Content == "" {
|
||||
return nil, errors.New("reservation tool returned no content")
|
||||
}
|
||||
}
|
||||
if m.failFirst {
|
||||
m.failFirst = false
|
||||
return nil, errors.New("simulated process interruption after checkpointed tool call")
|
||||
}
|
||||
return &ai.Response{Reply: "sku-123 is reserved; no duplicate reservation was made"}, nil
|
||||
}
|
||||
|
||||
func (m *demoModel) Stream(context.Context, *ai.Request, ...ai.GenerateOption) (ai.Stream, error) {
|
||||
return nil, ai.ErrStreamingUnsupported
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDurableAgentExampleResumesWithoutReplayingTool(t *testing.T) {
|
||||
out := captureStdout(t, main)
|
||||
if !strings.Contains(out, "simulated process interruption after checkpointed tool call") {
|
||||
t.Fatalf("example output %q did not show the initial interrupted run", out)
|
||||
}
|
||||
if !strings.Contains(out, "resumed reply: sku-123 is reserved; no duplicate reservation was made") {
|
||||
t.Fatalf("example output %q did not show the resumed response", out)
|
||||
}
|
||||
if !strings.Contains(out, "tool executions: 1") {
|
||||
t.Fatalf("example output %q did not prove the tool was not replayed", out)
|
||||
}
|
||||
}
|
||||
|
||||
func captureStdout(t *testing.T, fn func()) string {
|
||||
t.Helper()
|
||||
|
||||
old := os.Stdout
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("pipe stdout: %v", err)
|
||||
}
|
||||
os.Stdout = w
|
||||
|
||||
var buf bytes.Buffer
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
_, _ = io.Copy(&buf, r)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
fn()
|
||||
|
||||
_ = w.Close()
|
||||
os.Stdout = old
|
||||
<-done
|
||||
_ = r.Close()
|
||||
return buf.String()
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
# Agent Human Input Pause/Resume
|
||||
|
||||
Agents can pause a durable run when the model needs a human decision before it
|
||||
can continue. This keeps the services → agents → workflows lifecycle in one
|
||||
runtime: services expose tools, the agent decides it needs operator input, and
|
||||
the same checkpointed run resumes once that input arrives.
|
||||
|
||||
## Pattern
|
||||
|
||||
```go
|
||||
cp := flow.StoreCheckpoint(nil, "deploy-agent")
|
||||
ag := agent.New(
|
||||
agent.Name("deploy-agent"),
|
||||
agent.WithCheckpoint(cp),
|
||||
)
|
||||
|
||||
resp, err := ag.Ask(ctx, "Deploy the service")
|
||||
if err != nil {
|
||||
// If the model called the built-in request_input tool, the run is saved as
|
||||
// paused/input-required instead of losing state or completing early.
|
||||
pending, _ := agent.Pending(ctx, ag)
|
||||
runID := pending[0].ID
|
||||
|
||||
// Later, after an operator supplies the missing answer, the same run ID
|
||||
// continues with the original prompt, human input, memory, and completed
|
||||
// tool history intact.
|
||||
resp, err = agent.ResumeInput(ctx, ag, runID, "Deploy to us-east-1")
|
||||
}
|
||||
_ = resp
|
||||
```
|
||||
|
||||
The model sees a built-in `request_input` tool with a `prompt` argument. When it
|
||||
calls that tool, Go Micro persists the run with status `paused` and stage
|
||||
`input-required`. Plain `agent.Resume` continues to support completed, failed,
|
||||
and approval-paused runs; input-required runs are resumed with
|
||||
`agent.ResumeInput` so the human response is explicit.
|
||||
|
||||
## Cancellation and deadlines
|
||||
|
||||
`ResumeInput` uses the caller's `context.Context` for checkpoint reads, writes,
|
||||
and the resumed model/tool turn. If the context is canceled or its deadline
|
||||
expires before the resume is committed, the call returns the context error and
|
||||
the checkpointed run remains `paused` at `input-required`; list it with
|
||||
`agent.Pending` and retry with a fresh context once the operator is ready.
|
||||
@@ -0,0 +1,246 @@
|
||||
// Agent Ollama — a self-contained agent powered by Ollama Cloud.
|
||||
//
|
||||
// This example demonstrates the full harness loop — service tools, custom
|
||||
// tools, agent memory, guardrails, and streaming — using the Ollama
|
||||
// provider with gpt-oss:120b on Ollama Cloud.
|
||||
//
|
||||
// It creates a "knowledge" service with two endpoints (Add, Search) that
|
||||
// the agent discovers as tools, plus a custom "current_time" tool. The
|
||||
// agent answers natural-language questions by calling those tools.
|
||||
//
|
||||
// Run (Ollama Cloud — default):
|
||||
//
|
||||
// OLLAMA_API_KEY=your-key go run main.go
|
||||
//
|
||||
// Run (local Ollama):
|
||||
//
|
||||
// OLLAMA_BASE_URL=http://localhost:11434 \
|
||||
// OLLAMA_MODEL=llama3.2 \
|
||||
// go run main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
"go-micro.dev/v6/agent"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// knowledge service — a tiny in-memory knowledge base
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type KnowledgeEntry struct {
|
||||
ID string `json:"id" description:"Unique entry identifier"`
|
||||
Topic string `json:"topic" description:"Topic or category"`
|
||||
Content string `json:"content" description:"The knowledge content"`
|
||||
}
|
||||
|
||||
type AddKnowledgeRequest struct {
|
||||
Topic string `json:"topic" description:"Topic or category (required)"`
|
||||
Content string `json:"content" description:"The knowledge content (required)"`
|
||||
}
|
||||
|
||||
type AddKnowledgeResponse struct {
|
||||
Entry *KnowledgeEntry `json:"entry" description:"The added entry"`
|
||||
}
|
||||
|
||||
type SearchKnowledgeRequest struct {
|
||||
Topic string `json:"topic,omitempty" description:"Filter by topic (optional)"`
|
||||
Keyword string `json:"keyword,omitempty" description:"Search keyword in content (optional)"`
|
||||
}
|
||||
|
||||
type SearchKnowledgeResponse struct {
|
||||
Entries []*KnowledgeEntry `json:"entries" description:"Matching entries"`
|
||||
}
|
||||
|
||||
type KnowledgeService struct {
|
||||
mu sync.RWMutex
|
||||
entries []*KnowledgeEntry
|
||||
nextID int
|
||||
}
|
||||
|
||||
// Add stores a new knowledge entry.
|
||||
//
|
||||
// @example {"topic": "go", "content": "Go interfaces are implicit."}
|
||||
func (s *KnowledgeService) Add(ctx context.Context, req *AddKnowledgeRequest, rsp *AddKnowledgeResponse) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.nextID++
|
||||
e := &KnowledgeEntry{
|
||||
ID: fmt.Sprintf("kb-%d", s.nextID),
|
||||
Topic: req.Topic,
|
||||
Content: req.Content,
|
||||
}
|
||||
s.entries = append(s.entries, e)
|
||||
rsp.Entry = e
|
||||
return nil
|
||||
}
|
||||
|
||||
// Search finds knowledge entries by topic or keyword.
|
||||
//
|
||||
// @example {"topic": "go"}
|
||||
// @example {"keyword": "interface"}
|
||||
func (s *KnowledgeService) Search(ctx context.Context, req *SearchKnowledgeRequest, rsp *SearchKnowledgeResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, e := range s.entries {
|
||||
if req.Topic != "" && !strings.EqualFold(e.Topic, req.Topic) {
|
||||
continue
|
||||
}
|
||||
if req.Keyword != "" && !strings.Contains(strings.ToLower(e.Content), strings.ToLower(req.Keyword)) {
|
||||
continue
|
||||
}
|
||||
rsp.Entries = append(rsp.Entries, e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func main() {
|
||||
// Ollama Cloud is the default. Override with env vars for local Ollama.
|
||||
baseURL := os.Getenv("OLLAMA_BASE_URL")
|
||||
if baseURL == "" {
|
||||
baseURL = "https://ollama.com/v1"
|
||||
}
|
||||
model := os.Getenv("OLLAMA_MODEL")
|
||||
if model == "" {
|
||||
model = "gpt-oss:120b"
|
||||
}
|
||||
apiKey := os.Getenv("OLLAMA_API_KEY")
|
||||
|
||||
fmt.Println("╔══════════════════════════════════════════╗")
|
||||
fmt.Println("║ Ollama-Powered Go Micro Agent ║")
|
||||
fmt.Println("╚══════════════════════════════════════════╝")
|
||||
fmt.Println()
|
||||
fmt.Printf(" Ollama URL: %s\n", baseURL)
|
||||
fmt.Printf(" Model: %s\n", model)
|
||||
if apiKey != "" {
|
||||
fmt.Printf(" API Key: (set)\n")
|
||||
} else {
|
||||
fmt.Printf(" API Key: (none — set OLLAMA_API_KEY)\n")
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// 1. Start the knowledge service. Its handlers become agent tools.
|
||||
svc := micro.NewService("knowledge")
|
||||
svc.Handle(new(KnowledgeService))
|
||||
go svc.Run()
|
||||
|
||||
// Give the service a moment to register.
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// 2. Create the agent. It discovers the knowledge service endpoints
|
||||
// as tools automatically, plus gets a custom "current_time" tool.
|
||||
ag := micro.NewAgent("ollama-assistant",
|
||||
micro.AgentServices("knowledge"),
|
||||
micro.AgentPrompt(
|
||||
"You are a helpful knowledge assistant. You can search and add to "+
|
||||
"a knowledge base using the knowledge service tools. "+
|
||||
"When asked about the current time, use the current_time tool. "+
|
||||
"Be concise and factual.",
|
||||
),
|
||||
micro.AgentProvider("ollama"),
|
||||
micro.AgentModel(model),
|
||||
micro.AgentAPIKey(apiKey),
|
||||
micro.AgentBaseURL(baseURL),
|
||||
micro.AgentMaxSteps(10),
|
||||
micro.AgentLoopLimit(3),
|
||||
// Custom tool — any function, not tied to a service.
|
||||
agent.WithTool(
|
||||
"current_time",
|
||||
"Get the current date and time in a human-readable format",
|
||||
map[string]any{
|
||||
"timezone": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Optional timezone (defaults to local)",
|
||||
},
|
||||
},
|
||||
func(ctx context.Context, input map[string]any) (string, error) {
|
||||
tz, _ := input["timezone"].(string)
|
||||
if tz == "" {
|
||||
return time.Now().Format("2006-01-02 15:04:05 MST"), nil
|
||||
}
|
||||
loc, err := time.LoadLocation(tz)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unknown timezone: %s", tz)
|
||||
}
|
||||
return time.Now().In(loc).Format("2006-01-02 15:04:05 MST"), nil
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
// 3. Seed initial knowledge via the agent's first question.
|
||||
questions := []string{
|
||||
"What time is it now?",
|
||||
"Add a new knowledge entry: topic 'go', content 'Go interfaces are implicit — a type implements an interface by having the required methods.'",
|
||||
"Add another entry: topic 'go', content 'Go is a statically typed, compiled language designed at Google.'",
|
||||
"Add another entry: topic 'ai', content 'Large language models generate text by predicting the next token in a sequence.'",
|
||||
"Search the knowledge base for entries about Go.",
|
||||
"Search for everything in the knowledge base.",
|
||||
}
|
||||
|
||||
fmt.Println("─── Agent Demo ───")
|
||||
fmt.Println()
|
||||
|
||||
for i, q := range questions {
|
||||
fmt.Printf("Q%d: %s\n", i+1, q)
|
||||
fmt.Print("A: ")
|
||||
|
||||
resp, err := ag.Ask(context.Background(), q)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
fmt.Println()
|
||||
continue
|
||||
}
|
||||
|
||||
// Show tool calls the agent made.
|
||||
if len(resp.ToolCalls) > 0 {
|
||||
for _, tc := range resp.ToolCalls {
|
||||
args, _ := json.Marshal(tc.Input)
|
||||
fmt.Printf(" [tool] %s(%s)\n", tc.Name, string(args))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println(resp.Reply)
|
||||
if resp.Reply == "" && len(resp.ToolCalls) == 0 {
|
||||
fmt.Println("(no response)")
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// 4. Streaming demonstration.
|
||||
fmt.Println("─── Streaming Demo ───")
|
||||
fmt.Println()
|
||||
streamQ := "Explain what Go Micro is in two sentences."
|
||||
fmt.Printf("Q: %s\n", streamQ)
|
||||
fmt.Print("A: ")
|
||||
|
||||
stream, err := ag.Stream(context.Background(), streamQ)
|
||||
if err != nil {
|
||||
fmt.Printf("stream error: %v\n", err)
|
||||
} else {
|
||||
for {
|
||||
chunk, err := stream.Recv()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if chunk.Reply != "" {
|
||||
fmt.Print(chunk.Reply)
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("Done.")
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
# Agent Plan & Delegate
|
||||
|
||||
Demonstrates the two built-in agent capabilities — **planning** and **delegation** — in a small multi-agent system.
|
||||
|
||||
## What it shows
|
||||
|
||||
| Capability | Tool | What happens |
|
||||
|------------|------|--------------|
|
||||
| **Planning** | `plan` | The conductor records an ordered list of steps before doing multi-step work. The plan is saved to its store-backed memory and shown back to it on later turns. |
|
||||
| **Delegation** | `delegate` | The conductor hands the notification step to a separate `comms` agent. Because `comms` is a registered agent, the hand-off goes over RPC — not an in-process call. |
|
||||
|
||||
Both `plan` and `delegate` are added to every agent automatically. There's no harness or graph to configure: they're plain tools the model calls, the same as any service endpoint.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
task (service) Add, List ← owned by conductor
|
||||
notify (service) Send ← owned by comms
|
||||
comms (agent) manages notify
|
||||
conductor (agent) manages task, delegates notifications to comms
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
Set any provider key and run — the example auto-detects the provider:
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY=sk-ant-... # or OPENAI_API_KEY, GEMINI_API_KEY, ...
|
||||
go run main.go
|
||||
```
|
||||
|
||||
(You can also force a provider with `MICRO_AI_PROVIDER` / `MICRO_AI_API_KEY`.)
|
||||
|
||||
The conductor is asked to *"Create three launch tasks: Design, Build, and Ship. Then make sure owner@acme.com is notified that the launch plan is ready."*
|
||||
|
||||
Expected shape of the run:
|
||||
|
||||
```
|
||||
--- conductor tool calls ---
|
||||
→ plan({"steps":[{"task":"create Design task","status":"pending"}, ...]})
|
||||
→ task_TaskService_Add({"title":"Design"})
|
||||
→ task_TaskService_Add({"title":"Build"})
|
||||
→ task_TaskService_Add({"title":"Ship"})
|
||||
→ delegate({"task":"Notify owner@acme.com that the launch plan is ready","to":"comms"})
|
||||
📨 notify: to=owner@acme.com message="The launch plan is ready"
|
||||
|
||||
--- conductor reply ---
|
||||
Created the three launch tasks and asked comms to notify owner@acme.com.
|
||||
```
|
||||
|
||||
## Delegate-first
|
||||
|
||||
`delegate` is hybrid:
|
||||
|
||||
1. If `to` names a **registered agent** that owns the relevant services, the subtask is sent to it over RPC (`Agent.Chat`). That's what happens here — `comms` owns `notify`.
|
||||
2. Otherwise a focused **ephemeral sub-agent** is created for the subtask with a fresh, isolated context, asked the task, and torn down. Ephemeral sub-agents have no built-in tools, so they can't re-delegate.
|
||||
|
||||
This keeps intelligence distributed: the conductor doesn't need to know how to send notifications — it knows *who does*.
|
||||
@@ -0,0 +1,203 @@
|
||||
// Agent Plan & Delegate — planning and multi-agent delegation
|
||||
//
|
||||
// This example shows the two built-in agent capabilities:
|
||||
//
|
||||
// - plan: the conductor records an ordered plan before doing
|
||||
// multi-step work; the plan is saved to its memory.
|
||||
// - delegate: the conductor hands the notification step to a
|
||||
// separate "comms" agent over RPC, rather than doing it
|
||||
// itself.
|
||||
//
|
||||
// Two services (task, notify), two agents (conductor, comms). The
|
||||
// conductor manages task; comms manages notify. When asked to create
|
||||
// tasks and notify someone, the conductor plans the work, creates the
|
||||
// tasks with its own tools, then delegates the notification to comms —
|
||||
// which is a real registered agent, so the hand-off goes over RPC.
|
||||
//
|
||||
// Run (needs an LLM provider key):
|
||||
//
|
||||
// MICRO_AI_PROVIDER=anthropic MICRO_AI_API_KEY=sk-ant-... go run main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// task service
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type Task struct {
|
||||
ID string `json:"id" description:"Unique task identifier"`
|
||||
Title string `json:"title" description:"What the task is"`
|
||||
}
|
||||
|
||||
type AddRequest struct {
|
||||
Title string `json:"title" description:"Title of the task to add (required)"`
|
||||
}
|
||||
|
||||
type AddResponse struct {
|
||||
Task *Task `json:"task" description:"The created task"`
|
||||
}
|
||||
|
||||
type ListRequest struct{}
|
||||
|
||||
type ListResponse struct {
|
||||
Tasks []*Task `json:"tasks" description:"All tasks"`
|
||||
}
|
||||
|
||||
type TaskService struct {
|
||||
mu sync.Mutex
|
||||
tasks []*Task
|
||||
nextID int
|
||||
}
|
||||
|
||||
// Add creates a new task with the given title.
|
||||
//
|
||||
// @example {"title": "Design the launch page"}
|
||||
func (s *TaskService) Add(ctx context.Context, req *AddRequest, rsp *AddResponse) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.nextID++
|
||||
t := &Task{ID: fmt.Sprintf("task-%d", s.nextID), Title: req.Title}
|
||||
s.tasks = append(s.tasks, t)
|
||||
rsp.Task = t
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns all tasks.
|
||||
//
|
||||
// @example {}
|
||||
func (s *TaskService) List(ctx context.Context, req *ListRequest, rsp *ListResponse) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
rsp.Tasks = append(rsp.Tasks, s.tasks...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// notify service
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type SendRequest struct {
|
||||
To string `json:"to" description:"Recipient address (required)"`
|
||||
Message string `json:"message" description:"Message body (required)"`
|
||||
}
|
||||
|
||||
type SendResponse struct {
|
||||
Sent bool `json:"sent" description:"Whether the notification was sent"`
|
||||
}
|
||||
|
||||
type NotifyService struct{}
|
||||
|
||||
// Send delivers a notification message to a recipient.
|
||||
//
|
||||
// @example {"to": "owner@acme.com", "message": "The launch plan is ready"}
|
||||
func (s *NotifyService) Send(ctx context.Context, req *SendRequest, rsp *SendResponse) error {
|
||||
fmt.Printf(" 📨 notify: to=%s message=%q\n", req.To, req.Message)
|
||||
rsp.Sent = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
provider, apiKey := detectProvider()
|
||||
if apiKey == "" {
|
||||
fmt.Println("No LLM key found. Set a provider key and run again, e.g.:")
|
||||
fmt.Println(" export ANTHROPIC_API_KEY=sk-ant-... # or OPENAI_API_KEY, GEMINI_API_KEY, ...")
|
||||
fmt.Println(" go run main.go")
|
||||
return
|
||||
}
|
||||
fmt.Printf("Using provider %q\n", provider)
|
||||
|
||||
// Services.
|
||||
task := micro.NewService("task")
|
||||
task.Handle(new(TaskService))
|
||||
go task.Run()
|
||||
|
||||
notify := micro.NewService("notify")
|
||||
notify.Handle(new(NotifyService))
|
||||
go notify.Run()
|
||||
|
||||
// comms is a real, registered agent that owns the notify service.
|
||||
// Because it's registered, the conductor's delegate hand-off
|
||||
// reaches it over RPC.
|
||||
comms := micro.NewAgent("comms",
|
||||
micro.AgentServices("notify"),
|
||||
micro.AgentPrompt("You handle outbound notifications. Use the notify service to send messages."),
|
||||
micro.AgentProvider(provider),
|
||||
micro.AgentAPIKey(apiKey),
|
||||
)
|
||||
go comms.Run()
|
||||
|
||||
// The conductor owns task. Its prompt nudges it to plan, and to
|
||||
// delegate notifications to the comms agent rather than doing them.
|
||||
conductor := micro.NewAgent("conductor",
|
||||
micro.AgentServices("task"),
|
||||
micro.AgentPrompt(
|
||||
"You coordinate launch work. For multi-step requests, first call the plan tool "+
|
||||
"to record your steps, then carry them out. You can create tasks yourself. "+
|
||||
"For anything to do with notifying people, delegate to the \"comms\" agent "+
|
||||
"using the delegate tool (to: \"comms\") — do not try to notify directly.",
|
||||
),
|
||||
micro.AgentProvider(provider),
|
||||
micro.AgentAPIKey(apiKey),
|
||||
)
|
||||
|
||||
// Give the services and comms agent a moment to register.
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
resp, err := conductor.Ask(context.Background(),
|
||||
"Create three launch tasks: Design, Build, and Ship. "+
|
||||
"Then make sure owner@acme.com is notified that the launch plan is ready.")
|
||||
if err != nil {
|
||||
fmt.Println("error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("\n--- conductor tool calls ---")
|
||||
for _, tc := range resp.ToolCalls {
|
||||
args, _ := json.Marshal(tc.Input)
|
||||
fmt.Printf(" → %s(%s)\n", tc.Name, args)
|
||||
}
|
||||
fmt.Println("\n--- conductor reply ---")
|
||||
fmt.Println(resp.Reply)
|
||||
}
|
||||
|
||||
// detectProvider picks an LLM provider and key from the environment.
|
||||
// MICRO_AI_PROVIDER / MICRO_AI_API_KEY win if set; otherwise it falls
|
||||
// back to the first provider-specific key it finds (ANTHROPIC_API_KEY,
|
||||
// OPENAI_API_KEY, ...), so `export ANTHROPIC_API_KEY=... && go run .`
|
||||
// just works.
|
||||
func detectProvider() (provider, apiKey string) {
|
||||
provider = os.Getenv("MICRO_AI_PROVIDER")
|
||||
apiKey = os.Getenv("MICRO_AI_API_KEY")
|
||||
if apiKey != "" {
|
||||
if provider == "" {
|
||||
provider = "anthropic"
|
||||
}
|
||||
return provider, apiKey
|
||||
}
|
||||
|
||||
// provider name -> its conventional API key env var
|
||||
for _, p := range []struct{ name, env string }{
|
||||
{"anthropic", "ANTHROPIC_API_KEY"},
|
||||
{"openai", "OPENAI_API_KEY"},
|
||||
{"gemini", "GEMINI_API_KEY"},
|
||||
{"groq", "GROQ_API_KEY"},
|
||||
{"mistral", "MISTRAL_API_KEY"},
|
||||
{"together", "TOGETHER_API_KEY"},
|
||||
{"atlascloud", "ATLASCLOUD_API_KEY"},
|
||||
} {
|
||||
if v := os.Getenv(p.env); v != "" {
|
||||
return p.name, v
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
# Agent Tool Wrappers
|
||||
|
||||
Middleware around an agent's tool execution, the same way
|
||||
`client.CallWrapper` and `server.HandlerWrapper` wrap RPCs.
|
||||
|
||||
Every tool call an agent makes runs through `ai.ToolHandler`:
|
||||
|
||||
```go
|
||||
type ToolHandler func(ctx context.Context, call ai.ToolCall) ai.ToolResult
|
||||
type ToolWrapper func(ai.ToolHandler) ai.ToolHandler
|
||||
```
|
||||
|
||||
`WrapTool` (exposed as `micro.AgentWrapTool`) registers a wrapper: it
|
||||
takes the next handler and returns a new one. Code before `next(...)`
|
||||
runs before the tool, code after runs after. That single seam covers
|
||||
the whole lifecycle — before/after hooks, timing, metrics, retries,
|
||||
inspecting results.
|
||||
|
||||
## What this example does
|
||||
|
||||
One flaky `weather` service and one agent with two wrappers:
|
||||
|
||||
- **observe** — times every call and records a per-tool count, logging
|
||||
the correlation ID (`call.ID`) carried through from the provider. It
|
||||
observes; it changes nothing.
|
||||
- **retry** — re-runs a call whose result is an error, up to three
|
||||
attempts. The weather service fails the first time it's hit and
|
||||
succeeds after, so retry turns a transient failure into a success the
|
||||
model never sees.
|
||||
|
||||
Wrappers compose **outermost-first**: `observe` is registered first, so
|
||||
it wraps `retry` and sees one logical call even when retry runs the tool
|
||||
twice.
|
||||
|
||||
```go
|
||||
micro.NewAgent("forecaster",
|
||||
micro.AgentServices("weather"),
|
||||
micro.AgentProvider(provider),
|
||||
micro.AgentAPIKey(apiKey),
|
||||
micro.AgentWrapTool(m.observe, retry(3)),
|
||||
)
|
||||
```
|
||||
|
||||
## Wrappers vs. guardrails
|
||||
|
||||
Developer wrappers run **outside** the built-in guardrails (`MaxSteps`,
|
||||
`LoopLimit`, `ApproveTool`), so they see every call and its result —
|
||||
including a guardrail's refusal. The flip side: a retry wrapper's
|
||||
`next` is the full guardrail stack, so each retry is also counted by
|
||||
loop detection. Keep `LoopLimit` at or above your retry count, or set
|
||||
`AgentLoopLimit(0)` when a wrapper owns the repetition.
|
||||
|
||||
See the [Agent Guardrails guide](../../internal/website/docs/guides/agent-guardrails.md)
|
||||
for the full picture.
|
||||
|
||||
## Run
|
||||
|
||||
Needs an LLM provider key:
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY=sk-ant-... # or OPENAI_API_KEY, GEMINI_API_KEY, ...
|
||||
go run main.go
|
||||
```
|
||||
@@ -0,0 +1,227 @@
|
||||
// Agent Tool Wrappers — middleware around tool execution
|
||||
//
|
||||
// Every tool call an agent makes runs through ai.ToolHandler. WrapTool
|
||||
// wraps that handler the same way client.CallWrapper and
|
||||
// server.HandlerWrapper wrap RPCs: a wrapper takes the next handler and
|
||||
// returns a new one, so code before the next(...) call runs before the
|
||||
// tool and code after runs after.
|
||||
//
|
||||
// This example registers two wrappers on one agent:
|
||||
//
|
||||
// - observe: times every call and records a count per tool, keyed so
|
||||
// you can correlate by call ID. Pure "lifecycle hook" — it observes,
|
||||
// it doesn't change behavior.
|
||||
// - retry: re-runs a call whose result comes back as an error, up to
|
||||
// a few attempts. The "weather" service fails the first time it is
|
||||
// hit and succeeds after, so the retry wrapper turns a transient
|
||||
// failure into a success without the model ever seeing it.
|
||||
//
|
||||
// Wrappers compose outermost-first, so observe (registered first) wraps
|
||||
// retry: it sees one logical call even though retry may run it twice.
|
||||
//
|
||||
// Run (needs an LLM provider key):
|
||||
//
|
||||
// MICRO_AI_PROVIDER=anthropic MICRO_AI_API_KEY=sk-ant-... go run main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
"go-micro.dev/v6/ai"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// weather service — flaky on purpose
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type ForecastRequest struct {
|
||||
City string `json:"city" description:"City to get the forecast for (required)"`
|
||||
}
|
||||
|
||||
type ForecastResponse struct {
|
||||
City string `json:"city" description:"The city"`
|
||||
Forecast string `json:"forecast" description:"Human-readable forecast"`
|
||||
}
|
||||
|
||||
type WeatherService struct {
|
||||
mu sync.Mutex
|
||||
calls int
|
||||
}
|
||||
|
||||
// Forecast returns the weather forecast for a city. It fails on the very
|
||||
// first call (to simulate a transient dependency error) and succeeds
|
||||
// afterwards, so the retry wrapper has something to recover from.
|
||||
//
|
||||
// @example {"city": "London"}
|
||||
func (s *WeatherService) Forecast(ctx context.Context, req *ForecastRequest, rsp *ForecastResponse) error {
|
||||
s.mu.Lock()
|
||||
s.calls++
|
||||
n := s.calls
|
||||
s.mu.Unlock()
|
||||
|
||||
if n == 1 {
|
||||
return fmt.Errorf("weather upstream temporarily unavailable")
|
||||
}
|
||||
rsp.City = req.City
|
||||
rsp.Forecast = "18°C and clear"
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// wrappers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// metrics is collected by the observe wrapper. A real deployment would
|
||||
// emit these to OpenTelemetry or Prometheus; here we just print them.
|
||||
type metrics struct {
|
||||
mu sync.Mutex
|
||||
counts map[string]int
|
||||
took map[string]time.Duration
|
||||
}
|
||||
|
||||
func newMetrics() *metrics {
|
||||
return &metrics{counts: map[string]int{}, took: map[string]time.Duration{}}
|
||||
}
|
||||
|
||||
// observe times each tool call and records a per-tool count. It mirrors a
|
||||
// service-side metrics wrapper: measure around next(...), record, return
|
||||
// the result untouched.
|
||||
func (m *metrics) observe(next ai.ToolHandler) ai.ToolHandler {
|
||||
return func(ctx context.Context, call ai.ToolCall) ai.ToolResult {
|
||||
start := time.Now()
|
||||
res := next(ctx, call)
|
||||
took := time.Since(start)
|
||||
|
||||
m.mu.Lock()
|
||||
m.counts[call.Name]++
|
||||
m.took[call.Name] += took
|
||||
m.mu.Unlock()
|
||||
|
||||
fmt.Printf(" [observe] id=%s tool=%s took=%s\n", shortID(call.ID), call.Name, took.Round(time.Millisecond))
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
// retry re-runs a call whose result comes back as an error, up to
|
||||
// attempts times. Because it sits inside observe, the outer wrapper still
|
||||
// sees one logical call even though retry may run next more than once.
|
||||
//
|
||||
// Developer wrappers run outside the built-in guardrails, so next here is
|
||||
// the full guardrail stack: each retry is also seen by loop detection.
|
||||
// Keep LoopLimit at or above your retry count (the default 3 covers the
|
||||
// 2 attempts this example makes), or disable it with AgentLoopLimit(0)
|
||||
// when a wrapper is responsible for repetition.
|
||||
func retry(attempts int) ai.ToolWrapper {
|
||||
return func(next ai.ToolHandler) ai.ToolHandler {
|
||||
return func(ctx context.Context, call ai.ToolCall) ai.ToolResult {
|
||||
var res ai.ToolResult
|
||||
for i := 1; i <= attempts; i++ {
|
||||
res = next(ctx, call)
|
||||
if !isError(res) {
|
||||
return res
|
||||
}
|
||||
if i < attempts {
|
||||
fmt.Printf(" [retry] tool=%s attempt %d failed, retrying\n", call.Name, i)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isError reports whether a tool result is an error. The RPC handler
|
||||
// encodes failures as a JSON object with an "error" field in Content.
|
||||
func isError(res ai.ToolResult) bool {
|
||||
return strings.Contains(res.Content, `"error"`)
|
||||
}
|
||||
|
||||
func shortID(id string) string {
|
||||
if len(id) > 8 {
|
||||
return id[:8]
|
||||
}
|
||||
if id == "" {
|
||||
return "-"
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func main() {
|
||||
provider, apiKey := detectProvider()
|
||||
if apiKey == "" {
|
||||
fmt.Println("No LLM key found. Set a provider key and run again, e.g.:")
|
||||
fmt.Println(" export ANTHROPIC_API_KEY=sk-ant-... # or OPENAI_API_KEY, GEMINI_API_KEY, ...")
|
||||
fmt.Println(" go run main.go")
|
||||
return
|
||||
}
|
||||
fmt.Printf("Using provider %q\n", provider)
|
||||
|
||||
weather := micro.NewService("weather")
|
||||
weather.Handle(new(WeatherService))
|
||||
go weather.Run()
|
||||
|
||||
m := newMetrics()
|
||||
|
||||
agent := micro.NewAgent("forecaster",
|
||||
micro.AgentServices("weather"),
|
||||
micro.AgentPrompt("You report the weather. Use the weather service to answer."),
|
||||
micro.AgentProvider(provider),
|
||||
micro.AgentAPIKey(apiKey),
|
||||
// observe is registered first, so it is the outer wrapper and
|
||||
// retry is the inner one.
|
||||
micro.AgentWrapTool(m.observe, retry(3)),
|
||||
)
|
||||
|
||||
// Give the service a moment to register.
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
resp, err := agent.Ask(context.Background(), "What's the weather in London?")
|
||||
if err != nil {
|
||||
fmt.Println("error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("\n--- reply ---")
|
||||
fmt.Println(resp.Reply)
|
||||
|
||||
fmt.Println("\n--- tool metrics ---")
|
||||
m.mu.Lock()
|
||||
for name, n := range m.counts {
|
||||
fmt.Printf(" %s: %d call(s), total %s\n", name, n, m.took[name].Round(time.Millisecond))
|
||||
}
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// detectProvider picks an LLM provider and key from the environment.
|
||||
// MICRO_AI_PROVIDER / MICRO_AI_API_KEY win if set; otherwise it falls
|
||||
// back to the first provider-specific key it finds.
|
||||
func detectProvider() (provider, apiKey string) {
|
||||
provider = os.Getenv("MICRO_AI_PROVIDER")
|
||||
apiKey = os.Getenv("MICRO_AI_API_KEY")
|
||||
if apiKey != "" {
|
||||
if provider == "" {
|
||||
provider = "anthropic"
|
||||
}
|
||||
return provider, apiKey
|
||||
}
|
||||
|
||||
for _, p := range []struct{ name, env string }{
|
||||
{"anthropic", "ANTHROPIC_API_KEY"},
|
||||
{"openai", "OPENAI_API_KEY"},
|
||||
{"gemini", "GEMINI_API_KEY"},
|
||||
{"groq", "GROQ_API_KEY"},
|
||||
{"mistral", "MISTRAL_API_KEY"},
|
||||
{"together", "TOGETHER_API_KEY"},
|
||||
{"atlascloud", "ATLASCLOUD_API_KEY"},
|
||||
} {
|
||||
if v := os.Getenv(p.env); v != "" {
|
||||
return p.name, v
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Agent x402 buyer
|
||||
|
||||
This example shows an agent paying for a paid HTTP tool with x402 without using
|
||||
live funds or a live chain.
|
||||
|
||||
It starts a local paid endpoint guarded by `wrapper/x402` seller middleware and a
|
||||
mock facilitator. A deterministic mock-model agent calls that endpoint as a tool,
|
||||
receives the HTTP 402 challenge, pays with `AgentPayer`, stays inside
|
||||
`AgentBudget`, retries the request, and prints the spend recorded for the run.
|
||||
|
||||
```bash
|
||||
go run ./examples/agent-x402-buyer
|
||||
```
|
||||
|
||||
Expected output includes:
|
||||
|
||||
- the paid tool response,
|
||||
- one facilitator verify and settle call, and
|
||||
- `run spend: 7 smallest units (budget 10)`.
|
||||
|
||||
The payment token and facilitator are intentionally local development fakes. To
|
||||
settle real x402 payments, keep the same `AgentPayer` / `AgentBudget` shape but
|
||||
replace the payer with a wallet-backed implementation and configure the seller
|
||||
middleware with a hosted or self-run x402 facilitator.
|
||||
@@ -0,0 +1,168 @@
|
||||
// Agent x402 buyer — a provider-free example of an agent paying for a paid tool.
|
||||
//
|
||||
// Run:
|
||||
//
|
||||
// go run ./examples/agent-x402-buyer
|
||||
//
|
||||
// It starts a local HTTP tool protected by x402 middleware, then asks a
|
||||
// deterministic mock-model agent to call that tool. The agent receives the 402
|
||||
// challenge, uses AgentPayer and AgentBudget to pay within a local mock
|
||||
// facilitator, retries the request, and prints the run spend.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
go_micro "go-micro.dev/v6"
|
||||
"go-micro.dev/v6/agent"
|
||||
"go-micro.dev/v6/ai"
|
||||
"go-micro.dev/v6/store"
|
||||
"go-micro.dev/v6/wrapper/x402"
|
||||
)
|
||||
|
||||
const (
|
||||
paidToolName = "paid.market_brief"
|
||||
price = int64(7)
|
||||
paymentToken = "dev-payment-token"
|
||||
)
|
||||
|
||||
type devFacilitator struct {
|
||||
verifyCount int
|
||||
settleCount int
|
||||
}
|
||||
|
||||
func (f *devFacilitator) Verify(ctx context.Context, payment string, req x402.Requirements) (x402.Result, error) {
|
||||
f.verifyCount++
|
||||
if payment != paymentToken {
|
||||
return x402.Result{Valid: false, Reason: "unknown dev payment token"}, nil
|
||||
}
|
||||
return x402.Result{Valid: true, Payer: "dev-agent-wallet"}, nil
|
||||
}
|
||||
|
||||
func (f *devFacilitator) Settle(ctx context.Context, payment string, req x402.Requirements) (x402.Result, error) {
|
||||
f.settleCount++
|
||||
return x402.Result{Valid: true, Settlement: "dev-settlement-001"}, nil
|
||||
}
|
||||
|
||||
type devPayer struct{}
|
||||
|
||||
func (devPayer) Pay(ctx context.Context, req x402.Requirements) (string, error) {
|
||||
return paymentToken, nil
|
||||
}
|
||||
|
||||
type mockModel struct{ opts ai.Options }
|
||||
|
||||
func newMock(opts ...ai.Option) ai.Model {
|
||||
m := &mockModel{}
|
||||
_ = m.Init(opts...)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *mockModel) Init(opts ...ai.Option) error {
|
||||
for _, o := range opts {
|
||||
o(&m.opts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *mockModel) Options() ai.Options { return m.opts }
|
||||
func (m *mockModel) String() string { return "agent-x402-buyer-mock" }
|
||||
func (m *mockModel) Stream(context.Context, *ai.Request, ...ai.GenerateOption) (ai.Stream, error) {
|
||||
return nil, fmt.Errorf("stream not supported by agent-x402-buyer mock")
|
||||
}
|
||||
|
||||
func (m *mockModel) Generate(ctx context.Context, req *ai.Request, _ ...ai.GenerateOption) (*ai.Response, error) {
|
||||
for _, tool := range req.Tools {
|
||||
if tool.Name == paidToolName && m.opts.ToolHandler != nil {
|
||||
out := m.opts.ToolHandler(ctx, ai.ToolCall{ID: "paid-brief", Name: tool.Name, Input: map[string]any{"url": req.Prompt}})
|
||||
return &ai.Response{Answer: fmt.Sprintf("Paid tool returned: %s", out.Content)}, nil
|
||||
}
|
||||
}
|
||||
return &ai.Response{Answer: "No paid tool was available."}, nil
|
||||
}
|
||||
|
||||
func paidToolServer(fac *devFacilitator) *httptest.Server {
|
||||
mux := http.NewServeMux()
|
||||
paid := x402.Middleware(x402.Config{
|
||||
PayTo: "0xMerchantDevWallet",
|
||||
Network: "base-sepolia",
|
||||
Amount: fmt.Sprint(price),
|
||||
Description: "Local market brief for the x402 buyer example",
|
||||
Facilitator: fac,
|
||||
})
|
||||
mux.Handle("/brief", paid(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"brief": "Mock demand is up 12% after the agent paid the local tool.",
|
||||
"settlement": w.Header().Get(x402.PaymentResponseHeader),
|
||||
})
|
||||
})))
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func run(w io.Writer) error {
|
||||
ai.Register("agent-x402-buyer-mock", newMock)
|
||||
|
||||
fac := &devFacilitator{}
|
||||
srv := paidToolServer(fac)
|
||||
defer srv.Close()
|
||||
|
||||
st := store.NewMemoryStore()
|
||||
buyer := agent.New(
|
||||
agent.Name("x402-buyer"),
|
||||
agent.Provider("agent-x402-buyer-mock"),
|
||||
agent.Prompt("Call the paid market brief tool when given its URL."),
|
||||
agent.WithStore(st),
|
||||
go_micro.AgentPayer(devPayer{}),
|
||||
go_micro.AgentBudget(10),
|
||||
agent.WithTool(paidToolName, "Fetch a paid market brief over HTTP", map[string]any{
|
||||
"url": map[string]any{"type": "string", "description": "Paid HTTP endpoint to call"},
|
||||
}, func(ctx context.Context, input map[string]any) (string, error) {
|
||||
url, _ := input["url"].(string)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(body), nil
|
||||
}),
|
||||
)
|
||||
|
||||
resp, err := buyer.Ask(context.Background(), srv.URL+"/brief")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
events, err := agent.LoadRunEvents(st, "x402-buyer", resp.RunID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var spent int64
|
||||
for _, event := range events {
|
||||
if event.Spent > spent {
|
||||
spent = event.Spent
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, "Agent x402 buyer (provider: mock, funds: local dev token)")
|
||||
fmt.Fprintln(w, strings.TrimSpace(resp.Reply))
|
||||
fmt.Fprintf(w, "facilitator verify=%d settle=%d\n", fac.verifyCount, fac.settleCount)
|
||||
fmt.Fprintf(w, "run spend: %d smallest units (budget 10)\n", spent)
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := run(os.Stdout); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# Compiled binaries
|
||||
server/server
|
||||
client/client
|
||||
|
||||
# Test binaries
|
||||
*.test
|
||||
|
||||
# Output files
|
||||
*.out
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
@@ -0,0 +1,396 @@
|
||||
# Auth Example
|
||||
|
||||
This example demonstrates how to use the auth wrappers to protect your microservices with authentication and authorization.
|
||||
|
||||
## Overview
|
||||
|
||||
The example includes:
|
||||
|
||||
- **Server** - A Greeter service with:
|
||||
- Protected endpoint: `Greeter.Hello` (requires auth)
|
||||
- Public endpoint: `Greeter.Health` (no auth required)
|
||||
|
||||
- **Client** - Makes calls to the server:
|
||||
- With authentication (successful)
|
||||
- Without authentication (fails as expected)
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Client │
|
||||
│ ┌────────────────────────────────┐ │
|
||||
│ │ AuthClient Wrapper │ │
|
||||
│ │ - Adds Bearer token │ │
|
||||
│ │ - To all requests │ │
|
||||
│ └────────────────────────────────┘ │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│ RPC with Authorization: Bearer <token>
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Server │
|
||||
│ ┌────────────────────────────────┐ │
|
||||
│ │ AuthHandler Wrapper │ │
|
||||
│ │ - Extracts token │ │
|
||||
│ │ - Verifies with auth.Inspect()│ │
|
||||
│ │ - Checks with rules.Verify() │ │
|
||||
│ │ - Returns 401/403 if denied │ │
|
||||
│ └────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────┐ │
|
||||
│ │ Handler (Greeter.Hello) │ │
|
||||
│ │ - Gets account from context │ │
|
||||
│ │ - Processes request │ │
|
||||
│ └────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
examples/auth/
|
||||
├── README.md # This file
|
||||
├── proto/
|
||||
│ ├── greeter.proto # Service definition
|
||||
│ └── greeter.pb.go # Generated Go code
|
||||
├── server/
|
||||
│ └── main.go # Protected service
|
||||
└── client/
|
||||
└── main.go # Client with auth
|
||||
```
|
||||
|
||||
## Running the Example
|
||||
|
||||
### 1. Start the Server
|
||||
|
||||
```bash
|
||||
cd server
|
||||
go run main.go
|
||||
```
|
||||
|
||||
The server will:
|
||||
- Start the Greeter service
|
||||
- Apply auth wrapper to protect endpoints
|
||||
- Generate a test token and print it
|
||||
|
||||
Output:
|
||||
```
|
||||
=== Test Token Generated ===
|
||||
Use this token to test the client:
|
||||
TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... go run client/main.go
|
||||
|
||||
2026/02/11 10:00:00 Server [greeter] Listening on [::]:54321
|
||||
```
|
||||
|
||||
### 2. Run the Client (With Auth)
|
||||
|
||||
In a new terminal:
|
||||
|
||||
```bash
|
||||
cd client
|
||||
TOKEN=<token-from-server> go run main.go
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
=== Test 1: Protected endpoint WITH auth ===
|
||||
Response: Hello, test-user!
|
||||
|
||||
=== Test 2: Public endpoint (no auth needed) ===
|
||||
Health Status: ok
|
||||
|
||||
=== Test 3: Protected endpoint WITHOUT auth (should fail) ===
|
||||
Expected error: {"id":"greeter","code":401,"detail":"missing authorization token","status":"Unauthorized"}
|
||||
```
|
||||
|
||||
### 3. Run the Client (Without Auth)
|
||||
|
||||
```bash
|
||||
cd client
|
||||
go run main.go
|
||||
```
|
||||
|
||||
This will auto-generate a token for testing.
|
||||
|
||||
## Code Walkthrough
|
||||
|
||||
### Server Setup
|
||||
|
||||
```go
|
||||
// 1. Create auth provider
|
||||
// For this example we use the noop auth (accepts all tokens)
|
||||
// In production, use JWT or a custom auth provider
|
||||
authProvider := noop.NewAuth()
|
||||
|
||||
// 2. Create authorization rules
|
||||
rules := auth.NewRules()
|
||||
rules.Grant(&auth.Rule{
|
||||
ID: "public-health",
|
||||
Scope: "",
|
||||
Resource: &auth.Resource{Endpoint: "Greeter.Health"},
|
||||
Access: auth.AccessGranted,
|
||||
})
|
||||
|
||||
// 3. Wrap service with auth handler
|
||||
service := micro.NewService(
|
||||
micro.Name("greeter"),
|
||||
micro.WrapHandler(
|
||||
authWrapper.AuthHandler(authWrapper.HandlerOptions{
|
||||
Auth: authProvider,
|
||||
Rules: rules,
|
||||
SkipEndpoints: []string{"Greeter.Health"},
|
||||
}),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Client Setup
|
||||
|
||||
```go
|
||||
// 1. Get or generate token
|
||||
token := os.Getenv("TOKEN")
|
||||
|
||||
// 2. Wrap client with auth
|
||||
service := micro.NewService(
|
||||
micro.Name("greeter.client"),
|
||||
micro.WrapClient(
|
||||
authWrapper.FromToken(token),
|
||||
),
|
||||
)
|
||||
|
||||
// 3. Make calls (token automatically added)
|
||||
greeterClient := pb.NewGreeterService("greeter", service.Client())
|
||||
rsp, err := greeterClient.Hello(ctx, &pb.Request{Name: "John"})
|
||||
```
|
||||
|
||||
### Handler Implementation
|
||||
|
||||
```go
|
||||
func (g *Greeter) Hello(ctx context.Context, req *pb.Request, rsp *pb.Response) error {
|
||||
// Get account from context (added by auth wrapper)
|
||||
acc, ok := auth.AccountFromContext(ctx)
|
||||
if !ok {
|
||||
return errors.Unauthorized("greeter", "authentication required")
|
||||
}
|
||||
|
||||
rsp.Msg = "Hello, " + acc.ID + "!"
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Auth Wrapper Features
|
||||
|
||||
### Server Wrapper (`AuthHandler`)
|
||||
|
||||
- **Token Extraction**: Reads `Authorization: Bearer <token>` from metadata
|
||||
- **Token Verification**: Validates token using `auth.Inspect()`
|
||||
- **Authorization**: Checks permissions using `rules.Verify()`
|
||||
- **Context Injection**: Adds account to context for handlers
|
||||
- **Error Handling**: Returns 401/403 with clear error messages
|
||||
- **Skip Endpoints**: Allows public endpoints without auth
|
||||
|
||||
### Client Wrapper (`AuthClient`)
|
||||
|
||||
- **Automatic Token Injection**: Adds Bearer token to all requests
|
||||
- **Context-Aware**: Can extract account from context
|
||||
- **Static Token**: Use `FromToken()` for pre-generated tokens
|
||||
- **Dynamic Token**: Use `FromContext()` to generate per-request
|
||||
|
||||
## Auth Strategies
|
||||
|
||||
### 1. All Endpoints Protected
|
||||
|
||||
```go
|
||||
micro.WrapHandler(
|
||||
authWrapper.AuthRequired(authProvider, rules),
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Some Public Endpoints
|
||||
|
||||
```go
|
||||
micro.WrapHandler(
|
||||
authWrapper.PublicEndpoints(authProvider, rules, []string{
|
||||
"Health.Check",
|
||||
"Status.Version",
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Optional Auth (Extract but Don't Enforce)
|
||||
|
||||
```go
|
||||
micro.WrapHandler(
|
||||
authWrapper.AuthOptional(authProvider),
|
||||
)
|
||||
```
|
||||
|
||||
## Authorization Rules
|
||||
|
||||
### Grant Public Access
|
||||
|
||||
```go
|
||||
rules.Grant(&auth.Rule{
|
||||
ID: "public",
|
||||
Scope: "", // No scope = public
|
||||
Resource: &auth.Resource{Endpoint: "Health.Check"},
|
||||
Access: auth.AccessGranted,
|
||||
})
|
||||
```
|
||||
|
||||
### Require Authentication
|
||||
|
||||
```go
|
||||
rules.Grant(&auth.Rule{
|
||||
ID: "authenticated",
|
||||
Scope: "*", // Any authenticated user
|
||||
Resource: &auth.Resource{Endpoint: "*"},
|
||||
Access: auth.AccessGranted,
|
||||
})
|
||||
```
|
||||
|
||||
### Require Specific Scope
|
||||
|
||||
```go
|
||||
rules.Grant(&auth.Rule{
|
||||
ID: "admin-only",
|
||||
Scope: "admin", // Only admin scope
|
||||
Resource: &auth.Resource{Endpoint: "Admin.*"},
|
||||
Access: auth.AccessGranted,
|
||||
})
|
||||
```
|
||||
|
||||
### Deny Access
|
||||
|
||||
```go
|
||||
rules.Grant(&auth.Rule{
|
||||
ID: "deny-delete",
|
||||
Scope: "*",
|
||||
Resource: &auth.Resource{Endpoint: "User.Delete"},
|
||||
Access: auth.AccessDenied,
|
||||
Priority: 100, // Higher priority = evaluated first
|
||||
})
|
||||
```
|
||||
|
||||
## Testing Without Server
|
||||
|
||||
You can test auth logic without a running server:
|
||||
|
||||
```go
|
||||
import "go-micro.dev/v5/auth/noop"
|
||||
|
||||
// Create auth provider (noop for testing)
|
||||
authProvider := noop.NewAuth()
|
||||
|
||||
// Generate account
|
||||
acc, _ := authProvider.Generate("test-user", auth.WithScopes("admin"))
|
||||
|
||||
// Generate token
|
||||
token, _ := authProvider.Token(auth.WithCredentials(acc.ID, acc.Secret))
|
||||
|
||||
// Verify token
|
||||
verified, _ := authProvider.Inspect(token.AccessToken)
|
||||
fmt.Println(verified.ID) // Returns a generated UUID
|
||||
```
|
||||
|
||||
## Production Considerations
|
||||
|
||||
### 1. Use Production Auth Provider
|
||||
|
||||
The noop auth provider (`auth.NewAuth()`) is for development only. It accepts any token.
|
||||
|
||||
For production, implement a proper auth provider or use the JWT implementation:
|
||||
|
||||
```go
|
||||
// Option 1: Implement custom auth.Auth interface
|
||||
type MyAuth struct {
|
||||
// Your implementation
|
||||
}
|
||||
|
||||
func (m *MyAuth) Generate(id string, opts ...auth.GenerateOption) (*auth.Account, error) {
|
||||
// Generate real accounts
|
||||
}
|
||||
|
||||
func (m *MyAuth) Inspect(token string) (*auth.Account, error) {
|
||||
// Verify real tokens (JWT, OAuth, etc.)
|
||||
}
|
||||
|
||||
// Option 2: Use JWT auth (requires jwt package implementation)
|
||||
// Note: The jwt package in auth/jwt depends on an external plugin
|
||||
// You may need to implement your own JWT auth or use a third-party library
|
||||
```
|
||||
|
||||
### 3. Add Gateway Auth
|
||||
|
||||
If using HTTP gateway:
|
||||
|
||||
```go
|
||||
// Add auth to HTTP gateway
|
||||
http.Handle("/", gateway.Handler(
|
||||
gateway.WithAuth(authProvider),
|
||||
))
|
||||
```
|
||||
|
||||
### 4. Service-to-Service Auth
|
||||
|
||||
Services calling other services:
|
||||
|
||||
```go
|
||||
// Service A calls Service B with its own token
|
||||
client := micro.NewService(
|
||||
micro.WrapClient(
|
||||
authWrapper.FromContext(authProvider),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### 5. Token Refresh
|
||||
|
||||
```go
|
||||
// Check if token is expiring
|
||||
if time.Until(token.Expiry) < 5*time.Minute {
|
||||
token, _ = authProvider.Token(auth.WithToken(token.RefreshToken))
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "missing authorization token"
|
||||
|
||||
- **Cause**: Client didn't send Authorization header
|
||||
- **Fix**: Wrap client with `authWrapper.FromToken(token)`
|
||||
|
||||
### Error: "invalid token"
|
||||
|
||||
- **Cause**: Token is expired or malformed
|
||||
- **Fix**: Generate a new token
|
||||
|
||||
### Error: "access denied"
|
||||
|
||||
- **Cause**: Account doesn't have required permissions
|
||||
- **Fix**: Check authorization rules with `rules.List()`
|
||||
|
||||
### Error: "token verification failed"
|
||||
|
||||
- **Cause**: Server can't verify token (wrong keys, expired, etc.)
|
||||
- **Fix**: Ensure server and client use same auth provider
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Read the [Auth Documentation](/docs/auth)
|
||||
- Explore [JWT Auth](/auth/jwt)
|
||||
- Try [Custom Auth Provider](/examples/auth/custom)
|
||||
- See [Multi-Tenant Auth](/examples/auth/multi-tenant)
|
||||
|
||||
## Summary
|
||||
|
||||
The auth wrappers make it easy to:
|
||||
|
||||
1. **Protect services**: Add `WrapHandler(AuthHandler(...))`
|
||||
2. **Add authentication to clients**: Add `WrapClient(FromToken(...))`
|
||||
3. **Control access**: Define rules with `rules.Grant()`
|
||||
4. **Access account info**: Use `auth.AccountFromContext(ctx)`
|
||||
|
||||
That's it! Your microservices now have enterprise-grade authentication and authorization.
|
||||
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
"go-micro.dev/v6/auth"
|
||||
"go-micro.dev/v6/auth/noop"
|
||||
"go-micro.dev/v6/client"
|
||||
authWrapper "go-micro.dev/v6/wrapper/auth"
|
||||
|
||||
pb "go-micro.dev/v6/examples/auth/proto"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Get token from environment or generate one
|
||||
token := os.Getenv("TOKEN")
|
||||
|
||||
// Create auth provider (same as server)
|
||||
authProvider := noop.NewAuth()
|
||||
|
||||
// If no token provided, generate one
|
||||
if token == "" {
|
||||
log.Println("No TOKEN env var provided, generating test token...")
|
||||
acc, err := authProvider.Generate("test-user")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
t, err := authProvider.Token(auth.WithCredentials(acc.ID, acc.Secret))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
token = t.AccessToken
|
||||
log.Printf("Generated token: %s\n", token)
|
||||
}
|
||||
|
||||
// Create service with auth client wrapper
|
||||
service := micro.NewService("greeter.client", micro.WrapClient(
|
||||
authWrapper.FromToken(token), // Add token to all requests
|
||||
),
|
||||
)
|
||||
|
||||
service.Init()
|
||||
|
||||
// Create greeter client
|
||||
greeterClient := pb.NewGreeterService("greeter", service.Client())
|
||||
|
||||
// Test 1: Call protected endpoint (Hello) with auth
|
||||
fmt.Println("\n=== Test 1: Protected endpoint WITH auth ===")
|
||||
rsp, err := greeterClient.Hello(context.Background(), &pb.Request{Name: "John"})
|
||||
if err != nil {
|
||||
log.Printf("Error: %v", err)
|
||||
} else {
|
||||
fmt.Printf("Response: %s\n", rsp.Msg)
|
||||
}
|
||||
|
||||
// Test 2: Call public endpoint (Health) without auth
|
||||
fmt.Println("\n=== Test 2: Public endpoint (no auth needed) ===")
|
||||
// Create client without auth wrapper for this test
|
||||
plainClient := client.NewClient()
|
||||
plainGreeterClient := pb.NewGreeterService("greeter", plainClient)
|
||||
|
||||
healthRsp, err := plainGreeterClient.Health(context.Background(), &pb.HealthRequest{})
|
||||
if err != nil {
|
||||
log.Printf("Error: %v", err)
|
||||
} else {
|
||||
fmt.Printf("Health Status: %s\n", healthRsp.Status)
|
||||
}
|
||||
|
||||
// Test 3: Call protected endpoint WITHOUT auth (should fail)
|
||||
fmt.Println("\n=== Test 3: Protected endpoint WITHOUT auth (should fail) ===")
|
||||
_, err = plainGreeterClient.Hello(context.Background(), &pb.Request{Name: "John"})
|
||||
if err != nil {
|
||||
fmt.Printf("Expected error: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("Unexpected: Call succeeded without auth!")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// source: greeter.proto
|
||||
|
||||
package greeter
|
||||
|
||||
import (
|
||||
context "context"
|
||||
fmt "fmt"
|
||||
client "go-micro.dev/v6/client"
|
||||
server "go-micro.dev/v6/server"
|
||||
)
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ = fmt.Errorf
|
||||
|
||||
type Request struct {
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Request) Reset() { *m = Request{} }
|
||||
func (m *Request) String() string { return fmt.Sprintf("Request{Name:%s}", m.Name) }
|
||||
func (*Request) ProtoMessage() {}
|
||||
|
||||
func (m *Request) GetName() string {
|
||||
if m != nil {
|
||||
return m.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Msg string `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Response) Reset() { *m = Response{} }
|
||||
func (m *Response) String() string { return fmt.Sprintf("Response{Msg:%s}", m.Msg) }
|
||||
func (*Response) ProtoMessage() {}
|
||||
|
||||
func (m *Response) GetMsg() string {
|
||||
if m != nil {
|
||||
return m.Msg
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type HealthRequest struct{}
|
||||
|
||||
func (m *HealthRequest) Reset() { *m = HealthRequest{} }
|
||||
func (m *HealthRequest) String() string { return "HealthRequest{}" }
|
||||
func (*HealthRequest) ProtoMessage() {}
|
||||
|
||||
type HealthResponse struct {
|
||||
Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"`
|
||||
}
|
||||
|
||||
func (m *HealthResponse) Reset() { *m = HealthResponse{} }
|
||||
func (m *HealthResponse) String() string { return fmt.Sprintf("HealthResponse{Status:%s}", m.Status) }
|
||||
func (*HealthResponse) ProtoMessage() {}
|
||||
|
||||
func (m *HealthResponse) GetStatus() string {
|
||||
if m != nil {
|
||||
return m.Status
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Types registered
|
||||
}
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ context.Context
|
||||
var _ client.Option
|
||||
var _ server.Option
|
||||
|
||||
// Client API for Greeter service
|
||||
|
||||
type GreeterService interface {
|
||||
Hello(ctx context.Context, in *Request, opts ...client.CallOption) (*Response, error)
|
||||
Health(ctx context.Context, in *HealthRequest, opts ...client.CallOption) (*HealthResponse, error)
|
||||
}
|
||||
|
||||
type greeterService struct {
|
||||
c client.Client
|
||||
name string
|
||||
}
|
||||
|
||||
func NewGreeterService(name string, c client.Client) GreeterService {
|
||||
return &greeterService{
|
||||
c: c,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *greeterService) Hello(ctx context.Context, in *Request, opts ...client.CallOption) (*Response, error) {
|
||||
req := c.c.NewRequest(c.name, "Greeter.Hello", in)
|
||||
out := new(Response)
|
||||
err := c.c.Call(ctx, req, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *greeterService) Health(ctx context.Context, in *HealthRequest, opts ...client.CallOption) (*HealthResponse, error) {
|
||||
req := c.c.NewRequest(c.name, "Greeter.Health", in)
|
||||
out := new(HealthResponse)
|
||||
err := c.c.Call(ctx, req, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Server API for Greeter service
|
||||
|
||||
type GreeterHandler interface {
|
||||
Hello(context.Context, *Request, *Response) error
|
||||
Health(context.Context, *HealthRequest, *HealthResponse) error
|
||||
}
|
||||
|
||||
func RegisterGreeterHandler(s server.Server, hdlr GreeterHandler, opts ...server.HandlerOption) error {
|
||||
type greeter interface {
|
||||
Hello(ctx context.Context, in *Request, out *Response) error
|
||||
Health(ctx context.Context, in *HealthRequest, out *HealthResponse) error
|
||||
}
|
||||
type Greeter struct {
|
||||
greeter
|
||||
}
|
||||
h := &greeterHandler{hdlr}
|
||||
return s.Handle(s.NewHandler(&Greeter{h}, opts...))
|
||||
}
|
||||
|
||||
type greeterHandler struct {
|
||||
GreeterHandler
|
||||
}
|
||||
|
||||
func (h *greeterHandler) Hello(ctx context.Context, in *Request, out *Response) error {
|
||||
return h.GreeterHandler.Hello(ctx, in, out)
|
||||
}
|
||||
|
||||
func (h *greeterHandler) Health(ctx context.Context, in *HealthRequest, out *HealthResponse) error {
|
||||
return h.GreeterHandler.Health(ctx, in, out)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package greeter;
|
||||
|
||||
option go_package = "go-micro.dev/v5/examples/auth/proto;greeter";
|
||||
|
||||
service Greeter {
|
||||
rpc Hello(Request) returns (Response) {}
|
||||
rpc Health(HealthRequest) returns (HealthResponse) {}
|
||||
}
|
||||
|
||||
message Request {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message Response {
|
||||
string msg = 1;
|
||||
}
|
||||
|
||||
message HealthRequest {}
|
||||
|
||||
message HealthResponse {
|
||||
string status = 1;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
"go-micro.dev/v6/auth"
|
||||
"go-micro.dev/v6/auth/noop"
|
||||
authWrapper "go-micro.dev/v6/wrapper/auth"
|
||||
|
||||
pb "go-micro.dev/v6/examples/auth/proto"
|
||||
)
|
||||
|
||||
// Greeter implements the Greeter service
|
||||
type Greeter struct{}
|
||||
|
||||
// Hello is a protected endpoint that requires authentication
|
||||
func (g *Greeter) Hello(ctx context.Context, req *pb.Request, rsp *pb.Response) error {
|
||||
// Get account from context (added by auth wrapper)
|
||||
acc, ok := auth.AccountFromContext(ctx)
|
||||
if !ok {
|
||||
rsp.Msg = "Hello, anonymous!"
|
||||
return nil
|
||||
}
|
||||
|
||||
rsp.Msg = "Hello, " + acc.ID + "!"
|
||||
return nil
|
||||
}
|
||||
|
||||
// Health is a public endpoint that doesn't require auth
|
||||
func (g *Greeter) Health(ctx context.Context, req *pb.HealthRequest, rsp *pb.HealthResponse) error {
|
||||
rsp.Status = "ok"
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Create auth provider (noop for this example)
|
||||
// In production, use JWT or custom auth provider
|
||||
authProvider := noop.NewAuth()
|
||||
|
||||
// Create authorization rules
|
||||
rules := auth.NewRules()
|
||||
|
||||
// Grant public access to health endpoint
|
||||
rules.Grant(&auth.Rule{
|
||||
ID: "public-health",
|
||||
Scope: "",
|
||||
Resource: &auth.Resource{Type: "service", Name: "*", Endpoint: "Greeter.Health"},
|
||||
Access: auth.AccessGranted,
|
||||
Priority: 100,
|
||||
})
|
||||
|
||||
// Require authentication for other endpoints
|
||||
rules.Grant(&auth.Rule{
|
||||
ID: "authenticated-hello",
|
||||
Scope: "*",
|
||||
Resource: &auth.Resource{Type: "service", Name: "*", Endpoint: "*"},
|
||||
Access: auth.AccessGranted,
|
||||
Priority: 50,
|
||||
})
|
||||
|
||||
// Create service with auth wrapper
|
||||
service := micro.NewService("greeter", micro.Version("latest"),
|
||||
micro.WrapHandler(
|
||||
authWrapper.AuthHandler(authWrapper.HandlerOptions{
|
||||
Auth: authProvider,
|
||||
Rules: rules,
|
||||
SkipEndpoints: []string{"Greeter.Health"}, // Public endpoints
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
service.Init()
|
||||
|
||||
// Register handler
|
||||
if err := pb.RegisterGreeterHandler(service.Server(), &Greeter{}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Generate a test token for demonstration
|
||||
if acc, err := authProvider.Generate("test-user"); err == nil {
|
||||
if token, err := authProvider.Token(auth.WithCredentials(acc.ID, acc.Secret)); err == nil {
|
||||
log.Printf("\n=== Test Token Generated ===")
|
||||
log.Printf("Use this token to test the client:")
|
||||
log.Printf("TOKEN=%s go run client/main.go\n", token.AccessToken)
|
||||
}
|
||||
}
|
||||
|
||||
// Run service
|
||||
if err := service.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Multi-stage build for a go-micro service
|
||||
FROM golang:1.22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -o /service .
|
||||
|
||||
FROM alpine:3.19
|
||||
RUN apk --no-cache add ca-certificates
|
||||
COPY --from=builder /service /service
|
||||
ENTRYPOINT ["/service"]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Standalone MCP gateway
|
||||
FROM golang:1.22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -o /gateway ./cmd/gateway
|
||||
|
||||
FROM alpine:3.19
|
||||
RUN apk --no-cache add ca-certificates
|
||||
COPY --from=builder /gateway /gateway
|
||||
ENTRYPOINT ["/gateway"]
|
||||
@@ -0,0 +1,116 @@
|
||||
# Docker Compose Deployment Example
|
||||
|
||||
Run a go-micro service with MCP gateway, service registry, and distributed tracing in one command.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────┐ discover ┌──────────┐ RPC ┌─────────┐
|
||||
│ Agent │ ─────────────→ │ MCP │ ──────────→ │ Your │
|
||||
│ (Claude) │ MCP :3001 │ Gateway │ │ Service │
|
||||
└─────────┘ └──────────┘ └─────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────┐ ┌──────────┐
|
||||
│ Consul │ │ Jaeger │
|
||||
│ Registry │ │ Tracing │
|
||||
│ :8500 │ │ :16686 │
|
||||
└──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Service | URL |
|
||||
|---------|-----|
|
||||
| MCP Tools | http://localhost:3001/mcp/tools |
|
||||
| Consul UI | http://localhost:8500 |
|
||||
| Jaeger UI | http://localhost:16686 |
|
||||
| Service RPC | http://localhost:9090 |
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
# List MCP tools
|
||||
curl http://localhost:3001/mcp/tools | jq
|
||||
|
||||
# Call a tool
|
||||
curl -X POST http://localhost:3001/mcp/call \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"tool": "myservice.Handler.Method", "arguments": {"key": "value"}}'
|
||||
|
||||
# View traces in Jaeger
|
||||
open http://localhost:16686
|
||||
```
|
||||
|
||||
## Connect Claude Code
|
||||
|
||||
```bash
|
||||
# Claude Code can connect to the running MCP gateway
|
||||
# Add to your Claude Code MCP settings:
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-services": {
|
||||
"url": "http://localhost:3001/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Customizing
|
||||
|
||||
### Add Your Service
|
||||
|
||||
Replace the `app` service's build context with your service directory:
|
||||
|
||||
```yaml
|
||||
app:
|
||||
build:
|
||||
context: ../path/to/your/service
|
||||
dockerfile: Dockerfile
|
||||
```
|
||||
|
||||
### Add More Services
|
||||
|
||||
```yaml
|
||||
users:
|
||||
build: ./users
|
||||
environment:
|
||||
MICRO_REGISTRY: consul
|
||||
MICRO_REGISTRY_ADDRESS: consul:8500
|
||||
|
||||
orders:
|
||||
build: ./orders
|
||||
environment:
|
||||
MICRO_REGISTRY: consul
|
||||
MICRO_REGISTRY_ADDRESS: consul:8500
|
||||
```
|
||||
|
||||
All services register with Consul. The MCP gateway discovers them automatically.
|
||||
|
||||
### Add Redis Cache
|
||||
|
||||
```yaml
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
```
|
||||
|
||||
Then set `MICRO_CACHE_ADDRESS=redis:6379` on your service.
|
||||
|
||||
### Production Considerations
|
||||
|
||||
- Add health checks to each service
|
||||
- Use named volumes for Consul data persistence
|
||||
- Configure rate limiting on the MCP gateway
|
||||
- Set up TLS between services
|
||||
- Use secrets management for API keys
|
||||
@@ -0,0 +1,65 @@
|
||||
# Go Micro + MCP Gateway deployment with Docker Compose
|
||||
#
|
||||
# This runs:
|
||||
# 1. Consul — service registry (discovery)
|
||||
# 2. App — your go-micro service(s)
|
||||
# 3. MCP Gateway — standalone MCP gateway connected to Consul
|
||||
# 4. Jaeger — distributed tracing UI
|
||||
#
|
||||
# Usage:
|
||||
# docker-compose up
|
||||
#
|
||||
# Endpoints:
|
||||
# MCP Tools: http://localhost:3001/mcp/tools
|
||||
# Consul UI: http://localhost:8500
|
||||
# Jaeger UI: http://localhost:16686
|
||||
# Service: http://localhost:9090 (RPC)
|
||||
|
||||
services:
|
||||
# --- Service Registry ---
|
||||
consul:
|
||||
image: consul:1.15
|
||||
ports:
|
||||
- "8500:8500"
|
||||
command: agent -server -bootstrap-expect=1 -ui -client=0.0.0.0
|
||||
|
||||
# --- Your Go Micro Service ---
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "9090:9090"
|
||||
environment:
|
||||
MICRO_REGISTRY: consul
|
||||
MICRO_REGISTRY_ADDRESS: consul:8500
|
||||
MICRO_SERVER_ADDRESS: :9090
|
||||
depends_on:
|
||||
- consul
|
||||
restart: unless-stopped
|
||||
|
||||
# --- MCP Gateway (standalone) ---
|
||||
mcp-gateway:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.gateway
|
||||
ports:
|
||||
- "3001:3001"
|
||||
environment:
|
||||
MICRO_REGISTRY: consul
|
||||
MICRO_REGISTRY_ADDRESS: consul:8500
|
||||
MCP_ADDRESS: :3001
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4318
|
||||
depends_on:
|
||||
- consul
|
||||
- app
|
||||
restart: unless-stopped
|
||||
|
||||
# --- Tracing ---
|
||||
jaeger:
|
||||
image: jaegertracing/all-in-one:1.53
|
||||
ports:
|
||||
- "16686:16686" # UI
|
||||
- "4318:4318" # OTLP HTTP
|
||||
environment:
|
||||
COLLECTOR_OTLP_ENABLED: "true"
|
||||
@@ -0,0 +1,55 @@
|
||||
# First Agent
|
||||
|
||||
This is the smallest runnable service-backed agent in the repository. It sits
|
||||
between `micro new helloworld` and the full [`examples/support`](../support/)
|
||||
0→hero reference.
|
||||
|
||||
It runs with a deterministic mock model, so you do not need `ANTHROPIC_API_KEY`,
|
||||
`OPENAI_API_KEY`, or any other provider secret.
|
||||
|
||||
```bash
|
||||
go run ./examples/first-agent
|
||||
```
|
||||
|
||||
Expected transcript:
|
||||
|
||||
```text
|
||||
First agent (provider: mock, no API key)
|
||||
> Summarize my next steps
|
||||
[notes] listed starter notes
|
||||
assistant: Your first agent read the notes service and found three steps: install the CLI, run a service, then chat with an agent.
|
||||
✓ service-backed agent completed without provider secrets
|
||||
```
|
||||
|
||||
## What it demonstrates
|
||||
|
||||
- `notes` is a normal Go Micro service with one RPC method.
|
||||
- `assistant` is an agent scoped to that service via `agent.Services("notes")`.
|
||||
- The mock model requests the service tool through the normal agent tool handler.
|
||||
- The final answer proves the service → agent path without a live model key.
|
||||
|
||||
CI keeps this path runnable with:
|
||||
|
||||
```bash
|
||||
go test ./examples/first-agent
|
||||
```
|
||||
|
||||
## Next chat, inspect, and debug breadcrumbs
|
||||
|
||||
This example exits after one in-process `assistant.Ask` call so it stays tiny and
|
||||
provider-free. When you move from this transcript to a long-running agent, keep
|
||||
these commands nearby:
|
||||
|
||||
```bash
|
||||
micro run
|
||||
micro chat assistant --prompt "Summarize my next steps"
|
||||
micro inspect agent assistant
|
||||
micro agent doctor assistant
|
||||
```
|
||||
|
||||
Use the [no-secret first-agent guide](../../internal/website/docs/guides/no-secret-first-agent.md)
|
||||
to compare this transcript with the CLI demo, then keep the
|
||||
[debugging guide](../../internal/website/docs/guides/debugging-agents.md) open for
|
||||
preflight, doctor, inspect, and history checks. After that, continue to
|
||||
[`examples/support`](../support/) for the full services → agents → workflows
|
||||
lifecycle with a flow trigger and an approval gate.
|
||||
@@ -0,0 +1,161 @@
|
||||
// First Agent — the smallest runnable service-backed agent.
|
||||
//
|
||||
// Run:
|
||||
//
|
||||
// go run ./examples/first-agent
|
||||
//
|
||||
// It uses a deterministic mock model, so it needs no provider API key. The
|
||||
// point is to show the first agent shape: a service exposes a tool, an agent
|
||||
// discovers that service, the model asks to call the tool, and the agent returns
|
||||
// a final answer.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6/agent"
|
||||
"go-micro.dev/v6/ai"
|
||||
"go-micro.dev/v6/broker"
|
||||
"go-micro.dev/v6/client"
|
||||
"go-micro.dev/v6/registry"
|
||||
"go-micro.dev/v6/selector"
|
||||
"go-micro.dev/v6/service"
|
||||
"go-micro.dev/v6/store"
|
||||
)
|
||||
|
||||
type ListNotesRequest struct{}
|
||||
|
||||
type ListNotesResponse struct {
|
||||
Notes []string `json:"notes" description:"Notes the assistant can summarize"`
|
||||
}
|
||||
|
||||
type NotesService struct{ w io.Writer }
|
||||
|
||||
// List returns the starter notes the first agent can read.
|
||||
// @example {}
|
||||
func (s *NotesService) List(ctx context.Context, req *ListNotesRequest, rsp *ListNotesResponse) error {
|
||||
rsp.Notes = []string{"Install the micro CLI", "Run a service", "Chat with an agent"}
|
||||
fmt.Fprintln(s.w, " [notes] listed starter notes")
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockModel struct{ opts ai.Options }
|
||||
|
||||
func newMock(opts ...ai.Option) ai.Model {
|
||||
m := &mockModel{}
|
||||
_ = m.Init(opts...)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *mockModel) Init(opts ...ai.Option) error {
|
||||
for _, o := range opts {
|
||||
o(&m.opts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *mockModel) Options() ai.Options { return m.opts }
|
||||
func (m *mockModel) String() string { return "first-agent-mock" }
|
||||
func (m *mockModel) Stream(context.Context, *ai.Request, ...ai.GenerateOption) (ai.Stream, error) {
|
||||
return nil, fmt.Errorf("stream not supported by first-agent mock")
|
||||
}
|
||||
|
||||
func (m *mockModel) Generate(ctx context.Context, req *ai.Request, _ ...ai.GenerateOption) (*ai.Response, error) {
|
||||
for _, tool := range req.Tools {
|
||||
if strings.Contains(tool.Name, "List") && m.opts.ToolHandler != nil {
|
||||
m.opts.ToolHandler(ctx, ai.ToolCall{ID: "list-notes", Name: tool.Name, Input: map[string]any{}})
|
||||
break
|
||||
}
|
||||
}
|
||||
return &ai.Response{Answer: "Your first agent read the notes service and found three steps: install the CLI, run a service, then chat with an agent."}, nil
|
||||
}
|
||||
|
||||
func waitFor(reg registry.Registry, names ...string) error {
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for _, name := range names {
|
||||
for {
|
||||
if svcs, err := reg.GetService(name); err == nil && len(svcs) > 0 && len(svcs[0].Nodes) > 0 {
|
||||
break
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
return fmt.Errorf("timed out waiting for %s", name)
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runFirstAgent() error {
|
||||
return runFirstAgentWithWriter(os.Stdout)
|
||||
}
|
||||
|
||||
func runFirstAgentWithWriter(w io.Writer) error {
|
||||
ai.Register("first-agent-mock", newMock)
|
||||
|
||||
reg := registry.NewMemoryRegistry()
|
||||
br := broker.NewMemoryBroker()
|
||||
if err := br.Init(); err != nil {
|
||||
return fmt.Errorf("init broker: %w", err)
|
||||
}
|
||||
if err := br.Connect(); err != nil {
|
||||
return fmt.Errorf("connect broker: %w", err)
|
||||
}
|
||||
defer br.Disconnect()
|
||||
cl := client.NewClient(client.Registry(reg), client.Selector(selector.NewSelector(selector.Registry(reg))), client.Broker(br))
|
||||
|
||||
notes := service.New(service.Name("notes"), service.Address("127.0.0.1:0"), service.Registry(reg), service.Client(cl), service.Broker(br), service.HandleSignal(false))
|
||||
if err := notes.Handle(&NotesService{w: w}); err != nil {
|
||||
return fmt.Errorf("handle notes: %w", err)
|
||||
}
|
||||
svcErr := make(chan error, 1)
|
||||
go func() { svcErr <- notes.Run() }()
|
||||
defer notes.Server().Stop()
|
||||
|
||||
assistant := agent.New(
|
||||
agent.Name("assistant"),
|
||||
agent.Address("127.0.0.1:0"),
|
||||
agent.Services("notes"),
|
||||
agent.Prompt("You are a friendly first agent. Use the notes service before answering."),
|
||||
agent.Provider("first-agent-mock"),
|
||||
agent.WithRegistry(reg),
|
||||
agent.WithClient(cl),
|
||||
agent.WithBroker(br),
|
||||
agent.WithStore(store.NewMemoryStore()),
|
||||
)
|
||||
agentErr := make(chan error, 1)
|
||||
go func() { agentErr <- assistant.Run() }()
|
||||
defer assistant.Stop()
|
||||
|
||||
if err := waitFor(reg, "notes", "assistant"); err != nil {
|
||||
select {
|
||||
case runErr := <-svcErr:
|
||||
return fmt.Errorf("run notes: %w", runErr)
|
||||
case runErr := <-agentErr:
|
||||
return fmt.Errorf("run assistant: %w", runErr)
|
||||
default:
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, "First agent (provider: mock, no API key)")
|
||||
fmt.Fprintln(w, "> Summarize my next steps")
|
||||
resp, err := assistant.Ask(context.Background(), "Summarize my next steps")
|
||||
if err != nil {
|
||||
return fmt.Errorf("ask assistant: %w", err)
|
||||
}
|
||||
fmt.Fprintln(w, "assistant:", resp.Reply)
|
||||
fmt.Fprintln(w, "✓ service-backed agent completed without provider secrets")
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := runFirstAgent(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRunFirstAgent(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
if err := runFirstAgentWithWriter(&out); err != nil {
|
||||
t.Fatalf("first-agent example failed: %v", err)
|
||||
}
|
||||
|
||||
want := strings.TrimSpace(readExpectedTranscript(t))
|
||||
got := strings.TrimSpace(out.String())
|
||||
if got != want {
|
||||
t.Fatalf("first-agent transcript drifted from README.md\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadmeDocumentsNextBreadcrumbs(t *testing.T) {
|
||||
b, err := os.ReadFile("README.md")
|
||||
if err != nil {
|
||||
t.Fatalf("read README.md: %v", err)
|
||||
}
|
||||
readme := string(b)
|
||||
start := strings.Index(readme, "## Next chat, inspect, and debug breadcrumbs")
|
||||
if start < 0 {
|
||||
t.Fatal("README.md missing next chat, inspect, and debug breadcrumbs section")
|
||||
}
|
||||
section := readme[start:]
|
||||
for _, want := range []string{
|
||||
"micro run",
|
||||
"micro chat assistant --prompt \"Summarize my next steps\"",
|
||||
"micro inspect agent assistant",
|
||||
"micro agent doctor assistant",
|
||||
"no-secret-first-agent.md",
|
||||
"debugging-agents.md",
|
||||
"examples/support",
|
||||
} {
|
||||
if !strings.Contains(section, want) {
|
||||
t.Fatalf("README.md next breadcrumbs missing %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readExpectedTranscript(t *testing.T) string {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile("README.md")
|
||||
if err != nil {
|
||||
t.Fatalf("read README.md: %v", err)
|
||||
}
|
||||
readme := string(b)
|
||||
const fence = "```text"
|
||||
start := strings.Index(readme, "Expected transcript:")
|
||||
if start < 0 {
|
||||
t.Fatal("README.md missing Expected transcript section")
|
||||
}
|
||||
fenceStart := strings.Index(readme[start:], fence)
|
||||
if fenceStart < 0 {
|
||||
t.Fatal("README.md missing transcript text fence")
|
||||
}
|
||||
start += fenceStart + len(fence)
|
||||
end := strings.Index(readme[start:], "```")
|
||||
if end < 0 {
|
||||
t.Fatal("README.md missing closing transcript fence")
|
||||
}
|
||||
return readme[start : start+end]
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
# Durable Flow
|
||||
|
||||
A workflow that survives a crash and resumes where it stopped.
|
||||
|
||||
A `flow` can be an ordered list of **steps** — a task with stages —
|
||||
instead of a single LLM turn. Each step is checkpointed before and after
|
||||
through a pluggable `Checkpoint` (store-backed by default), so if the
|
||||
process dies mid-run, the run resumes at the step it stopped on, without
|
||||
re-running the steps that already completed (and already had their side
|
||||
effects).
|
||||
|
||||
## What this shows
|
||||
|
||||
A three-step checkout (`reserve → charge → confirm`) whose `charge` step
|
||||
fails the first time, simulating a transient outage / crash:
|
||||
|
||||
```
|
||||
first run:
|
||||
reserve → inventory reserved
|
||||
charge → payment dependency unavailable (crash)
|
||||
run failed: payment gateway timeout
|
||||
|
||||
checkpoint: run 70643f61 is at step "charge" (status failed)
|
||||
|
||||
resume:
|
||||
charge → payment captured
|
||||
confirm → order confirmed
|
||||
|
||||
reserve ran 1 time(s) total — completed steps are not repeated on resume
|
||||
no pending runs — the workflow completed durably
|
||||
```
|
||||
|
||||
The key line is the last pair: on `Resume`, `reserve` does **not** run
|
||||
again — its result was checkpointed — and the run finishes.
|
||||
|
||||
## The pieces
|
||||
|
||||
```go
|
||||
f := micro.NewFlow("checkout",
|
||||
micro.FlowSteps(
|
||||
micro.FlowStep{Name: "reserve", Run: reserve},
|
||||
micro.FlowStep{Name: "charge", Run: charge},
|
||||
micro.FlowStep{Name: "confirm", Run: confirm},
|
||||
),
|
||||
micro.FlowWithCheckpoint(micro.StoreCheckpoint(nil, "checkout")), // nil store = default; "checkout" = key scope
|
||||
)
|
||||
|
||||
f.Execute(ctx, `{}`) // runs; crashes at charge
|
||||
pending, _ := f.Pending(ctx) // the run, checkpointed at "charge"
|
||||
f.Resume(ctx, pending[0].ID) // continues from charge to the end
|
||||
```
|
||||
|
||||
- **`State`** carries a typed payload (`Set`/`Scan`) plus a `Stage`
|
||||
marker — the resume point.
|
||||
- **`Checkpoint`** persists each `Run`. The built-in is store-backed and
|
||||
keeps each flow's runs in their own store table (database `flow`, table
|
||||
`checkout`) via `store.Scope`, so one flow's runs don't share a table
|
||||
with another's — or with agent or service state. Point the default
|
||||
store at Postgres or NATS KV and a run survives a real process restart,
|
||||
or implement the interface to plug in Temporal, Restate, etc.
|
||||
- A real step would be `flow.Call(service, endpoint)` (an RPC),
|
||||
`flow.Dispatch(agent)` (hand off to an agent), or `flow.LLM(prompt)`
|
||||
(one model turn). Here they're plain funcs so durability is the only
|
||||
thing on display.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
go run main.go
|
||||
```
|
||||
|
||||
No LLM key required.
|
||||
@@ -0,0 +1,114 @@
|
||||
// Durable Flow — a workflow that survives a crash and resumes
|
||||
//
|
||||
// A flow can be an ordered list of steps (a task with stages) rather than
|
||||
// a single LLM turn. Each step is checkpointed before and after through a
|
||||
// pluggable Checkpoint (store-backed by default), so if the process dies
|
||||
// mid-run, the run resumes at the step it stopped on — without re-running
|
||||
// the steps that already completed (and already had their side effects).
|
||||
//
|
||||
// This example needs no LLM key. It runs a three-step checkout flow whose
|
||||
// "charge" step fails the first time (a transient outage). The run is
|
||||
// checkpointed as failed at that step; we then "recover" the dependency
|
||||
// and Resume — and the already-completed "reserve" step does not run
|
||||
// again. A real step would call a service (flow.Call), an agent
|
||||
// (flow.Dispatch), or the model (flow.LLM); here they're plain funcs so
|
||||
// the durability is the only thing on display.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
)
|
||||
|
||||
// Order is the payload carried across steps via State.Set / State.Scan.
|
||||
type Order struct {
|
||||
ID string `json:"id"`
|
||||
Reserved bool `json:"reserved"`
|
||||
Charged bool `json:"charged"`
|
||||
Confirmed bool `json:"confirmed"`
|
||||
}
|
||||
|
||||
// charged toggles the transient failure: 0 = the payment dependency is
|
||||
// down (first run), 1 = recovered (on resume).
|
||||
var charged int
|
||||
|
||||
// reserveCalls proves the completed step is not re-run on resume.
|
||||
var reserveCalls int
|
||||
|
||||
func reserve(_ context.Context, in micro.FlowState) (micro.FlowState, error) {
|
||||
reserveCalls++
|
||||
var o Order
|
||||
in.Scan(&o)
|
||||
o.ID = "order-1"
|
||||
o.Reserved = true
|
||||
fmt.Println(" reserve → inventory reserved")
|
||||
return in, in.Set(o)
|
||||
}
|
||||
|
||||
func charge(_ context.Context, in micro.FlowState) (micro.FlowState, error) {
|
||||
var o Order
|
||||
in.Scan(&o)
|
||||
if charged == 0 {
|
||||
fmt.Println(" charge → payment dependency unavailable (crash)")
|
||||
return in, errors.New("payment gateway timeout")
|
||||
}
|
||||
o.Charged = true
|
||||
fmt.Println(" charge → payment captured")
|
||||
return in, in.Set(o)
|
||||
}
|
||||
|
||||
func confirm(_ context.Context, in micro.FlowState) (micro.FlowState, error) {
|
||||
var o Order
|
||||
in.Scan(&o)
|
||||
o.Confirmed = true
|
||||
fmt.Println(" confirm → order confirmed")
|
||||
return in, in.Set(o)
|
||||
}
|
||||
|
||||
func main() {
|
||||
f := micro.NewFlow("checkout",
|
||||
micro.FlowSteps(
|
||||
micro.FlowStep{Name: "reserve", Run: reserve},
|
||||
micro.FlowStep{Name: "charge", Run: charge},
|
||||
micro.FlowStep{Name: "confirm", Run: confirm},
|
||||
),
|
||||
// Durable by default; shown explicitly. Runs are namespaced under
|
||||
// the flow name ("flow/checkout/runs/..."), so this flow's state
|
||||
// doesn't share a keyspace with other flows. Point the default
|
||||
// store at Postgres or NATS KV to survive a real process restart.
|
||||
micro.FlowWithCheckpoint(micro.StoreCheckpoint(nil, "checkout")),
|
||||
)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
fmt.Println("first run:")
|
||||
if err := f.Execute(ctx, `{}`); err != nil {
|
||||
fmt.Printf(" run failed: %v\n", err)
|
||||
}
|
||||
|
||||
pending, _ := f.Pending(ctx)
|
||||
if len(pending) == 0 {
|
||||
fmt.Println("nothing pending — unexpected")
|
||||
return
|
||||
}
|
||||
run := pending[0]
|
||||
fmt.Printf("\ncheckpoint: run %s is at step %q (status %s)\n",
|
||||
run.ID[:8], run.State.Stage, run.Status)
|
||||
|
||||
// The dependency recovers (or a new process picks the run up).
|
||||
charged = 1
|
||||
|
||||
fmt.Println("\nresume:")
|
||||
if err := f.Resume(ctx, run.ID); err != nil {
|
||||
fmt.Printf(" resume failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\nreserve ran %d time(s) total — completed steps are not repeated on resume\n", reserveCalls)
|
||||
if pend, _ := f.Pending(ctx); len(pend) == 0 {
|
||||
fmt.Println("no pending runs — the workflow completed durably")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Agentic Loop — keep working until the goal is met, with a guaranteed ceiling
|
||||
//
|
||||
// The "loop" pattern from agentic AI: instead of one shot, run a step over
|
||||
// and over until the goal is reached, letting it decide when to stop — but
|
||||
// always bounded by a hard iteration cap (the guardrail) so it can never run
|
||||
// away, or run up an unbounded bill.
|
||||
//
|
||||
// flow.Loop is just a flow step, so it composes with the normal checkpointed
|
||||
// step engine. This example needs no LLM key: the body is a plain func that
|
||||
// "improves a draft" each pass, and a code-defined Until stops it once the
|
||||
// draft is good enough — capped by FlowLoopMax. In a real flow the body would
|
||||
// be micro.FlowDispatch("coder") (an agent) or micro.FlowLLM(...), and the
|
||||
// stop check micro.FlowUntilLLM("Is the work complete?") — the supervised
|
||||
// "Ralph" loop, where the model decides it's done but the cap still bounds it.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
)
|
||||
|
||||
// Draft is the payload carried across iterations via State.Set / State.Scan.
|
||||
type Draft struct {
|
||||
Text string `json:"text"`
|
||||
Quality int `json:"quality"` // 0..100, improved each pass
|
||||
}
|
||||
|
||||
// improve is one loop pass: it refines the draft a bit. In a real flow this
|
||||
// would be an agent or an LLM turn; here it's deterministic so the example
|
||||
// runs offline.
|
||||
func improve(_ context.Context, in micro.FlowState) (micro.FlowState, error) {
|
||||
var d Draft
|
||||
_ = in.Scan(&d)
|
||||
d.Quality += 30
|
||||
d.Text = fmt.Sprintf("draft refined (quality %d)", d.Quality)
|
||||
return in, in.Set(d)
|
||||
}
|
||||
|
||||
func main() {
|
||||
const goodEnough = 90
|
||||
|
||||
f := micro.NewFlow("refine",
|
||||
micro.FlowSteps(
|
||||
micro.FlowStep{Name: "improve", Run: micro.FlowLoop(
|
||||
improve,
|
||||
// Stop early once the draft is good enough...
|
||||
micro.FlowUntil(func(_ context.Context, s micro.FlowState, iter int) (bool, error) {
|
||||
var d Draft
|
||||
_ = s.Scan(&d)
|
||||
fmt.Printf(" pass %d → quality %d\n", iter, d.Quality)
|
||||
return d.Quality >= goodEnough, nil
|
||||
}),
|
||||
// ...but never run the body more than 10 times (the ceiling).
|
||||
micro.FlowLoopMax(10),
|
||||
)},
|
||||
),
|
||||
micro.FlowDeleteOnSuccess(),
|
||||
)
|
||||
|
||||
fmt.Println("refining until quality >=", goodEnough)
|
||||
if err := f.Execute(context.Background(), `{"text":"initial draft","quality":0}`); err != nil {
|
||||
fmt.Println("flow error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, r := range f.Results() {
|
||||
fmt.Printf("\ndone: %s\n", r.Answer)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# Graceful Stop Demo
|
||||
|
||||
This example demonstrates the intended shutdown behavior after the gRPC graceful-stop patch.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
go run ./examples/graceful-stop
|
||||
```
|
||||
|
||||
## Expected behavior
|
||||
|
||||
- one long-running RPC starts
|
||||
- shutdown begins while that RPC is still running
|
||||
- new RPCs stop being accepted shortly after shutdown starts
|
||||
- the in-flight RPC is allowed to finish
|
||||
|
||||
Typical output:
|
||||
|
||||
```text
|
||||
long RPC is running; starting shutdown
|
||||
new RPC rejected after shutdown began: ...
|
||||
long RPC completed: slept for 1500ms
|
||||
done
|
||||
```
|
||||
|
||||
There may be a small race window where the first post-stop RPC is still accepted once before subsequent new RPCs are rejected. The important part is that in-flight RPCs are drained while new RPCs are cut off.
|
||||
|
||||
## Automated check
|
||||
|
||||
```bash
|
||||
go test ./server/grpc -run TestGracefulStopRejectsNewRPCsButAllowsInFlightRPCs -v
|
||||
```
|
||||
|
||||
## Environment
|
||||
|
||||
- no special environment variables are required
|
||||
- the demo may print a TLS warning from `go-micro`; it is unrelated to this change
|
||||
@@ -0,0 +1,129 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
micro "go-micro.dev/v6"
|
||||
"go-micro.dev/v6/client"
|
||||
grpcclient "go-micro.dev/v6/client/grpc"
|
||||
"go-micro.dev/v6/registry"
|
||||
"go-micro.dev/v6/server"
|
||||
grpcserver "go-micro.dev/v6/server/grpc"
|
||||
)
|
||||
|
||||
type SleepRequest struct {
|
||||
DelayMS int `json:"delay_ms"`
|
||||
}
|
||||
|
||||
type SleepResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type Sleeper struct {
|
||||
started chan struct{}
|
||||
}
|
||||
|
||||
func (s *Sleeper) Sleep(ctx context.Context, req *SleepRequest, rsp *SleepResponse) error {
|
||||
select {
|
||||
case s.started <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
|
||||
timer := time.NewTimer(time.Duration(req.DelayMS) * time.Millisecond)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
|
||||
rsp.Message = fmt.Sprintf("slept for %dms", req.DelayMS)
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
addr := listener.Addr().String()
|
||||
reg := registry.NewMemoryRegistry()
|
||||
handler := &Sleeper{started: make(chan struct{}, 1)}
|
||||
|
||||
service := micro.NewService("grace-demo",
|
||||
micro.HandleSignal(false),
|
||||
micro.Registry(reg),
|
||||
micro.Server(grpcserver.NewServer(
|
||||
server.Registry(reg),
|
||||
server.Name("grace-demo"),
|
||||
server.Address(addr),
|
||||
grpcserver.Listener(listener),
|
||||
grpcserver.GracefulStopTimeout(3*time.Second),
|
||||
)),
|
||||
micro.Client(grpcclient.NewClient(
|
||||
client.Registry(reg),
|
||||
client.ContentType("application/grpc+json"),
|
||||
client.DialTimeout(200*time.Millisecond),
|
||||
client.RequestTimeout(5*time.Second),
|
||||
)),
|
||||
)
|
||||
|
||||
if err := service.Handle(handler); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := service.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Printf("service started on %s", addr)
|
||||
|
||||
longDone := make(chan error, 1)
|
||||
go func() {
|
||||
req := service.Client().NewRequest("grace-demo", "Sleeper.Sleep", &SleepRequest{DelayMS: 1500})
|
||||
rsp := &SleepResponse{}
|
||||
longDone <- service.Client().Call(context.Background(), req, rsp, client.WithAddress(addr))
|
||||
if rsp.Message != "" {
|
||||
log.Printf("long RPC completed: %s", rsp.Message)
|
||||
}
|
||||
}()
|
||||
|
||||
<-handler.started
|
||||
log.Printf("long RPC is running; starting shutdown")
|
||||
|
||||
stopDone := make(chan error, 1)
|
||||
go func() {
|
||||
stopDone <- service.Stop()
|
||||
}()
|
||||
|
||||
deadline := time.Now().Add(500 * time.Millisecond)
|
||||
for time.Now().Before(deadline) {
|
||||
callCtx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
|
||||
req := service.Client().NewRequest("grace-demo", "Sleeper.Sleep", &SleepRequest{DelayMS: 50})
|
||||
rsp := &SleepResponse{}
|
||||
err = service.Client().Call(callCtx, req, rsp, client.WithAddress(addr))
|
||||
cancel()
|
||||
if err != nil {
|
||||
log.Printf("new RPC rejected after shutdown began: %v", err)
|
||||
break
|
||||
}
|
||||
|
||||
log.Printf("new RPC still accepted during shutdown: %s", rsp.Message)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
if err := <-longDone; err != nil {
|
||||
log.Fatalf("long RPC failed: %v", err)
|
||||
}
|
||||
if err := <-stopDone; err != nil {
|
||||
log.Fatalf("service stop failed: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("done")
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
# gRPC Interop Example
|
||||
|
||||
This example shows that **any standard gRPC client** can call a go-micro service — no go-micro SDK required on the client side.
|
||||
|
||||
The server is a normal go-micro service using the gRPC transport. The client is a plain `google.golang.org/grpc` client with no go-micro imports.
|
||||
|
||||
## How it works
|
||||
|
||||
go-micro's gRPC server uses a [`grpc.UnknownServiceHandler`](https://pkg.go.dev/google.golang.org/grpc#UnknownServiceHandler) that catches all incoming requests and routes them by parsing the standard gRPC method path (`/package.Service/Method`). This means any language with gRPC support (Python, Java, Rust, etc.) can call go-micro services using generated protobuf stubs.
|
||||
|
||||
## Running
|
||||
|
||||
Start the server:
|
||||
|
||||
```bash
|
||||
cd examples/grpc-interop
|
||||
go run ./server/
|
||||
```
|
||||
|
||||
In another terminal, call it with the standard gRPC client:
|
||||
|
||||
```bash
|
||||
cd examples/grpc-interop
|
||||
go run ./client/ --name Alice
|
||||
# Response: Hello Alice
|
||||
```
|
||||
|
||||
## Calling from other languages
|
||||
|
||||
Generate stubs from `proto/greeter.proto` in your language of choice and point them at `localhost:50051`. For example, with Python:
|
||||
|
||||
```bash
|
||||
pip install grpcio-tools
|
||||
python -m grpc_tools.protoc -Iproto --python_out=. --grpc_python_out=. proto/greeter.proto
|
||||
```
|
||||
|
||||
```python
|
||||
import grpc
|
||||
import greeter_pb2
|
||||
import greeter_pb2_grpc
|
||||
|
||||
channel = grpc.insecure_channel("localhost:50051")
|
||||
stub = greeter_pb2_grpc.GreeterStub(channel)
|
||||
|
||||
response = stub.Hello(greeter_pb2.HelloRequest(name="Alice"))
|
||||
print(response.message) # Hello Alice
|
||||
```
|
||||
|
||||
## Key points
|
||||
|
||||
- The go-micro server registers handlers via `pb.RegisterGreeterHandler()`
|
||||
- The standard gRPC client uses stubs generated by `protoc-gen-go-grpc`
|
||||
- Both share the same `.proto` file — that's the contract
|
||||
- The server uses protobuf encoding on the wire, same as any gRPC service
|
||||
- Service discovery (mDNS, consul, etc.) is only needed for go-micro-to-go-micro calls; direct gRPC clients connect by address
|
||||
|
||||
## Regenerating proto stubs
|
||||
|
||||
```bash
|
||||
protoc --go_out=. --go_opt=paths=source_relative \
|
||||
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
||||
--micro_out=. --micro_opt=paths=source_relative \
|
||||
proto/greeter.proto
|
||||
```
|
||||
|
||||
Requires `protoc-gen-go`, `protoc-gen-go-grpc`, and `protoc-gen-micro`.
|
||||
@@ -0,0 +1,43 @@
|
||||
// client calls the go-micro Greeter service using a standard gRPC
|
||||
// client — no go-micro SDK. This proves any language with gRPC support
|
||||
// can call go-micro services.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
pb "example/proto"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
func main() {
|
||||
addr := flag.String("addr", "localhost:50051", "server address")
|
||||
name := flag.String("name", "World", "name to greet")
|
||||
flag.Parse()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
conn, err := grpc.NewClient(*addr,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("connect: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := pb.NewGreeterClient(conn)
|
||||
|
||||
resp, err := client.Hello(ctx, &pb.HelloRequest{Name: *name})
|
||||
if err != nil {
|
||||
log.Fatalf("Hello: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Response: %s\n", resp.Message)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
module example
|
||||
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
go-micro.dev/v5 v5.16.0
|
||||
google.golang.org/grpc v1.71.1
|
||||
google.golang.org/protobuf v1.36.6
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/bitly/go-simplejson v0.5.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||
github.com/cornelk/hashmap v1.0.8 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/consul/api v1.32.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-hclog v1.5.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/hashicorp/serf v0.10.1 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.50 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/hashstructure v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // 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
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rabbitmq/amqp091-go v1.10.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/urfave/cli/v2 v2.27.6 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
go.etcd.io/bbolt v1.4.0 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.5.21 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.5.21 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
golang.org/x/tools v0.31.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
)
|
||||
|
||||
replace go-micro.dev/v5 => ../..
|
||||
@@ -0,0 +1,387 @@
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
|
||||
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
|
||||
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cornelk/hashmap v1.0.8 h1:nv0AWgw02n+iDcawr5It4CjQIAcdMMKRrs10HOJYlrc=
|
||||
github.com/cornelk/hashmap v1.0.8/go.mod h1:RfZb7JO3RviW/rT6emczVuC/oxpdz4UsSB2LJSclR1k=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
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/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
|
||||
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
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.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/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/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
|
||||
github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
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/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE=
|
||||
github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4=
|
||||
github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg=
|
||||
github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
|
||||
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
|
||||
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
|
||||
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
|
||||
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
|
||||
github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM=
|
||||
github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
|
||||
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
|
||||
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
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-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
||||
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
||||
github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q=
|
||||
github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
|
||||
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
|
||||
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/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0=
|
||||
github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nats-io/jwt/v2 v2.7.4 h1:jXFuDDxs/GQjGDZGhNgH4tXzSUK6WQi2rsj4xmsNOtI=
|
||||
github.com/nats-io/jwt/v2 v2.7.4/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA=
|
||||
github.com/nats-io/nats-server/v2 v2.11.3 h1:AbGtXxuwjo0gBroLGGr/dE0vf24kTKdRnBq/3z/Fdoc=
|
||||
github.com/nats-io/nats-server/v2 v2.11.3/go.mod h1:6Z6Fd+JgckqzKig7DYwhgrE7bJ6fypPHnGPND+DqgMY=
|
||||
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/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
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/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE=
|
||||
github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
|
||||
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
|
||||
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
|
||||
go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8=
|
||||
go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs=
|
||||
go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY=
|
||||
go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU=
|
||||
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.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/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
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.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.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=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
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-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
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.4.2/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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/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.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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.0.0-20210220032951-036812b2e83c/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-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/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.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/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/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
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.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
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-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
|
||||
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/grpc/examples v0.0.0-20250515150734-f2d3e11f3057 h1:lPv+iqlAyiKMjbL3ivJlAASixPknLv806R6zaoE4PUM=
|
||||
google.golang.org/grpc/examples v0.0.0-20250515150734-f2d3e11f3057/go.mod h1:WPWnet+nYurNGpV0rVYHI1YuOJwVHeM3t8f76m410XM=
|
||||
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/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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=
|
||||
@@ -0,0 +1,173 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v3.21.12
|
||||
// source: proto/greeter.proto
|
||||
|
||||
package proto
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
type HelloRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *HelloRequest) Reset() {
|
||||
*x = HelloRequest{}
|
||||
mi := &file_proto_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_proto_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_proto_greeter_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *HelloRequest) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type HelloResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *HelloResponse) Reset() {
|
||||
*x = HelloResponse{}
|
||||
mi := &file_proto_greeter_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *HelloResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*HelloResponse) ProtoMessage() {}
|
||||
|
||||
func (x *HelloResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_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 HelloResponse.ProtoReflect.Descriptor instead.
|
||||
func (*HelloResponse) Descriptor() ([]byte, []int) {
|
||||
return file_proto_greeter_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *HelloResponse) GetMessage() string {
|
||||
if x != nil {
|
||||
return x.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_proto_greeter_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_proto_greeter_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x13proto/greeter.proto\x12\agreeter\"\"\n" +
|
||||
"\fHelloRequest\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\")\n" +
|
||||
"\rHelloResponse\x12\x18\n" +
|
||||
"\amessage\x18\x01 \x01(\tR\amessage2C\n" +
|
||||
"\aGreeter\x128\n" +
|
||||
"\x05Hello\x12\x15.greeter.HelloRequest\x1a\x16.greeter.HelloResponse\"\x00B\tZ\a./protob\x06proto3"
|
||||
|
||||
var (
|
||||
file_proto_greeter_proto_rawDescOnce sync.Once
|
||||
file_proto_greeter_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_proto_greeter_proto_rawDescGZIP() []byte {
|
||||
file_proto_greeter_proto_rawDescOnce.Do(func() {
|
||||
file_proto_greeter_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_greeter_proto_rawDesc), len(file_proto_greeter_proto_rawDesc)))
|
||||
})
|
||||
return file_proto_greeter_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_proto_greeter_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_proto_greeter_proto_goTypes = []any{
|
||||
(*HelloRequest)(nil), // 0: greeter.HelloRequest
|
||||
(*HelloResponse)(nil), // 1: greeter.HelloResponse
|
||||
}
|
||||
var file_proto_greeter_proto_depIdxs = []int32{
|
||||
0, // 0: greeter.Greeter.Hello:input_type -> greeter.HelloRequest
|
||||
1, // 1: greeter.Greeter.Hello:output_type -> greeter.HelloResponse
|
||||
1, // [1:2] is the sub-list for method output_type
|
||||
0, // [0:1] 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_proto_greeter_proto_init() }
|
||||
func file_proto_greeter_proto_init() {
|
||||
if File_proto_greeter_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_greeter_proto_rawDesc), len(file_proto_greeter_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_proto_greeter_proto_goTypes,
|
||||
DependencyIndexes: file_proto_greeter_proto_depIdxs,
|
||||
MessageInfos: file_proto_greeter_proto_msgTypes,
|
||||
}.Build()
|
||||
File_proto_greeter_proto = out.File
|
||||
file_proto_greeter_proto_goTypes = nil
|
||||
file_proto_greeter_proto_depIdxs = nil
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Code generated by protoc-gen-micro. DO NOT EDIT.
|
||||
// source: proto/greeter.proto
|
||||
|
||||
package proto
|
||||
|
||||
import (
|
||||
fmt "fmt"
|
||||
proto "google.golang.org/protobuf/proto"
|
||||
math "math"
|
||||
)
|
||||
|
||||
import (
|
||||
context "context"
|
||||
client "go-micro.dev/v6/client"
|
||||
server "go-micro.dev/v6/server"
|
||||
)
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ = proto.Marshal
|
||||
var _ = fmt.Errorf
|
||||
var _ = math.Inf
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ context.Context
|
||||
var _ client.Option
|
||||
var _ server.Option
|
||||
|
||||
// Client API for Greeter service
|
||||
|
||||
type GreeterService interface {
|
||||
Hello(ctx context.Context, in *HelloRequest, opts ...client.CallOption) (*HelloResponse, error)
|
||||
}
|
||||
|
||||
type greeterService struct {
|
||||
c client.Client
|
||||
name string
|
||||
}
|
||||
|
||||
func NewGreeterService(name string, c client.Client) GreeterService {
|
||||
return &greeterService{
|
||||
c: c,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *greeterService) Hello(ctx context.Context, in *HelloRequest, opts ...client.CallOption) (*HelloResponse, error) {
|
||||
req := c.c.NewRequest(c.name, "Greeter.Hello", in)
|
||||
out := new(HelloResponse)
|
||||
err := c.c.Call(ctx, req, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Server API for Greeter service
|
||||
|
||||
type GreeterHandler interface {
|
||||
Hello(context.Context, *HelloRequest, *HelloResponse) error
|
||||
}
|
||||
|
||||
func RegisterGreeterHandler(s server.Server, hdlr GreeterHandler, opts ...server.HandlerOption) error {
|
||||
type greeter interface {
|
||||
Hello(ctx context.Context, in *HelloRequest, out *HelloResponse) error
|
||||
}
|
||||
type Greeter struct {
|
||||
greeter
|
||||
}
|
||||
h := &greeterHandler{hdlr}
|
||||
return s.Handle(s.NewHandler(&Greeter{h}, opts...))
|
||||
}
|
||||
|
||||
type greeterHandler struct {
|
||||
GreeterHandler
|
||||
}
|
||||
|
||||
func (h *greeterHandler) Hello(ctx context.Context, in *HelloRequest, out *HelloResponse) error {
|
||||
return h.GreeterHandler.Hello(ctx, in, out)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package greeter;
|
||||
|
||||
option go_package = "./proto";
|
||||
|
||||
service Greeter {
|
||||
rpc Hello(HelloRequest) returns (HelloResponse) {}
|
||||
}
|
||||
|
||||
message HelloRequest {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message HelloResponse {
|
||||
string message = 1;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v3.21.12
|
||||
// source: proto/greeter.proto
|
||||
|
||||
package proto
|
||||
|
||||
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 (
|
||||
Greeter_Hello_FullMethodName = "/greeter.Greeter/Hello"
|
||||
)
|
||||
|
||||
// GreeterClient is the client API for Greeter 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.
|
||||
type GreeterClient interface {
|
||||
Hello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error)
|
||||
}
|
||||
|
||||
type greeterClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient {
|
||||
return &greeterClient{cc}
|
||||
}
|
||||
|
||||
func (c *greeterClient) Hello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(HelloResponse)
|
||||
err := c.cc.Invoke(ctx, Greeter_Hello_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GreeterServer is the server API for Greeter service.
|
||||
// All implementations must embed UnimplementedGreeterServer
|
||||
// for forward compatibility.
|
||||
type GreeterServer interface {
|
||||
Hello(context.Context, *HelloRequest) (*HelloResponse, error)
|
||||
mustEmbedUnimplementedGreeterServer()
|
||||
}
|
||||
|
||||
// UnimplementedGreeterServer must 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 UnimplementedGreeterServer struct{}
|
||||
|
||||
func (UnimplementedGreeterServer) Hello(context.Context, *HelloRequest) (*HelloResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method Hello not implemented")
|
||||
}
|
||||
func (UnimplementedGreeterServer) mustEmbedUnimplementedGreeterServer() {}
|
||||
func (UnimplementedGreeterServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeGreeterServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to GreeterServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeGreeterServer interface {
|
||||
mustEmbedUnimplementedGreeterServer()
|
||||
}
|
||||
|
||||
func RegisterGreeterServer(s grpc.ServiceRegistrar, srv GreeterServer) {
|
||||
// If the following call panics, it indicates UnimplementedGreeterServer 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(&Greeter_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _Greeter_Hello_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.(GreeterServer).Hello(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Greeter_Hello_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(GreeterServer).Hello(ctx, req.(*HelloRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// Greeter_ServiceDesc is the grpc.ServiceDesc for Greeter service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var Greeter_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "greeter.Greeter",
|
||||
HandlerType: (*GreeterServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "Hello",
|
||||
Handler: _Greeter_Hello_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "proto/greeter.proto",
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// server starts a go-micro gRPC service. Any standard gRPC client
|
||||
// (Go, Python, Java, etc.) can call it — no go-micro SDK required on
|
||||
// the client side.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
micro "go-micro.dev/v6"
|
||||
"go-micro.dev/v6/client"
|
||||
grpcclient "go-micro.dev/v6/client/grpc"
|
||||
"go-micro.dev/v6/server"
|
||||
grpcserver "go-micro.dev/v6/server/grpc"
|
||||
|
||||
pb "example/proto"
|
||||
)
|
||||
|
||||
type Greeter struct{}
|
||||
|
||||
func (g *Greeter) Hello(ctx context.Context, req *pb.HelloRequest, rsp *pb.HelloResponse) error {
|
||||
log.Printf("Received request: name=%q", req.Name)
|
||||
rsp.Message = "Hello " + req.Name
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
addr := ":50051"
|
||||
|
||||
service := micro.NewService("greeter",
|
||||
micro.Server(grpcserver.NewServer(
|
||||
server.Name("greeter"),
|
||||
server.Address(addr),
|
||||
)),
|
||||
micro.Client(grpcclient.NewClient(
|
||||
client.ContentType("application/grpc+proto"),
|
||||
)),
|
||||
)
|
||||
|
||||
service.Init()
|
||||
|
||||
pb.RegisterGreeterHandler(service.Server(), new(Greeter))
|
||||
|
||||
fmt.Println("Go-Micro gRPC server listening on", addr)
|
||||
fmt.Println()
|
||||
fmt.Println("Call with a standard gRPC client:")
|
||||
fmt.Println(" go run ../client/main.go")
|
||||
|
||||
if err := service.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
# Hello World Example
|
||||
|
||||
The simplest go-micro service demonstrating core concepts.
|
||||
|
||||
## What It Does
|
||||
|
||||
This example creates a basic RPC service that:
|
||||
- Listens on port 8080
|
||||
- Exposes a `Greeter.Hello` method
|
||||
- Returns a greeting message
|
||||
- Demonstrates both programmatic and HTTP access
|
||||
|
||||
## Run It
|
||||
|
||||
```bash
|
||||
go run main.go
|
||||
```
|
||||
|
||||
The service will start and make test calls to itself, then wait for incoming requests.
|
||||
|
||||
## Test It
|
||||
|
||||
### Using curl
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080 \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Micro-Endpoint: Greeter.Hello' \
|
||||
-d '{"name": "Alice"}'
|
||||
```
|
||||
|
||||
Expected response:
|
||||
```json
|
||||
{"message": "Hello Alice"}
|
||||
```
|
||||
|
||||
### Using the micro CLI
|
||||
|
||||
```bash
|
||||
micro call greeter Greeter.Hello '{"name": "Bob"}'
|
||||
```
|
||||
|
||||
## Code Walkthrough
|
||||
|
||||
1. **Define types** - Request and Response structures
|
||||
2. **Implement handler** - The `Greeter` service with `Hello` method
|
||||
3. **Create service** - Using `micro.NewService()` with options
|
||||
4. **Register handler** - Link the handler to the service
|
||||
5. **Run service** - Start listening for requests
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **RPC Pattern**: Method signature `func(ctx, req, rsp) error`
|
||||
- **Service Discovery**: Automatic registration
|
||||
- **Multiple Transports**: Works over HTTP, gRPC, etc.
|
||||
- **Type Safety**: Strongly typed requests/responses
|
||||
|
||||
## Next Steps
|
||||
|
||||
- See [pubsub-events](../pubsub-events/) for event-driven patterns
|
||||
- See [production-ready](../production-ready/) for a complete example
|
||||
- Read the [Getting Started Guide](../../internal/website/docs/getting-started.md)
|
||||
@@ -0,0 +1,70 @@
|
||||
module example
|
||||
|
||||
go 1.24
|
||||
|
||||
require go-micro.dev/v5 v5.16.0
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/bitly/go-simplejson v0.5.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||
github.com/cornelk/hashmap v1.0.8 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/consul/api v1.32.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-hclog v1.5.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/hashicorp/serf v0.10.1 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.50 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/hashstructure v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // 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
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rabbitmq/amqp091-go v1.10.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/urfave/cli/v2 v2.27.6 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
go.etcd.io/bbolt v1.4.0 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.5.21 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.5.21 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
golang.org/x/tools v0.31.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/grpc v1.71.1 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
)
|
||||
|
||||
replace go-micro.dev/v5 => ../..
|
||||
@@ -0,0 +1,385 @@
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
|
||||
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
|
||||
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cornelk/hashmap v1.0.8 h1:nv0AWgw02n+iDcawr5It4CjQIAcdMMKRrs10HOJYlrc=
|
||||
github.com/cornelk/hashmap v1.0.8/go.mod h1:RfZb7JO3RviW/rT6emczVuC/oxpdz4UsSB2LJSclR1k=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
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/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
|
||||
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
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.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/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/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
|
||||
github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
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/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE=
|
||||
github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4=
|
||||
github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg=
|
||||
github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
|
||||
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
|
||||
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
|
||||
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
|
||||
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
|
||||
github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM=
|
||||
github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
|
||||
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
|
||||
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
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-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
||||
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
||||
github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q=
|
||||
github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
|
||||
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
|
||||
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/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0=
|
||||
github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nats-io/jwt/v2 v2.7.4 h1:jXFuDDxs/GQjGDZGhNgH4tXzSUK6WQi2rsj4xmsNOtI=
|
||||
github.com/nats-io/jwt/v2 v2.7.4/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA=
|
||||
github.com/nats-io/nats-server/v2 v2.11.3 h1:AbGtXxuwjo0gBroLGGr/dE0vf24kTKdRnBq/3z/Fdoc=
|
||||
github.com/nats-io/nats-server/v2 v2.11.3/go.mod h1:6Z6Fd+JgckqzKig7DYwhgrE7bJ6fypPHnGPND+DqgMY=
|
||||
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/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
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/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE=
|
||||
github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
|
||||
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
|
||||
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
|
||||
go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8=
|
||||
go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs=
|
||||
go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY=
|
||||
go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU=
|
||||
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.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/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
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.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.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=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
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-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
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.4.2/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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/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.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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.0.0-20210220032951-036812b2e83c/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-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/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.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/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/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
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.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
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-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
|
||||
google.golang.org/grpc v1.71.1/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/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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=
|
||||
@@ -0,0 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
)
|
||||
|
||||
// Request and Response types
|
||||
type Request struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Greeter service handler
|
||||
type Greeter struct{}
|
||||
|
||||
// Hello is the RPC method handler
|
||||
func (g *Greeter) Hello(ctx context.Context, req *Request, rsp *Response) error {
|
||||
rsp.Message = "Hello " + req.Name
|
||||
log.Printf("Received request: %s", req.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Create a new service
|
||||
service := micro.NewService("greeter", micro.Address(":8080"))
|
||||
|
||||
// Initialize the service
|
||||
service.Init()
|
||||
|
||||
// Register the handler
|
||||
if err := service.Handle(new(Greeter)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println("Starting greeter service on :8080")
|
||||
fmt.Println()
|
||||
fmt.Println("Test with:")
|
||||
fmt.Println(" curl -XPOST \\")
|
||||
fmt.Println(" -H 'Content-Type: application/json' \\")
|
||||
fmt.Println(" -H 'Micro-Endpoint: Greeter.Hello' \\")
|
||||
fmt.Println(" -d '{\"name\": \"Alice\"}' \\")
|
||||
fmt.Println(" http://localhost:8080")
|
||||
|
||||
// Run the service
|
||||
if err := service.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
# MCP Examples
|
||||
|
||||
Examples demonstrating Model Context Protocol (MCP) integration with go-micro.
|
||||
|
||||
## Examples
|
||||
|
||||
### [hello](./hello/) - Minimal Example ⭐ Start Here
|
||||
|
||||
The simplest possible MCP-enabled service. Perfect for learning the basics.
|
||||
|
||||
**What it shows:**
|
||||
- Automatic documentation extraction from Go comments
|
||||
- MCP gateway setup with 3 lines
|
||||
- Ready for Claude Code
|
||||
|
||||
**Run it:**
|
||||
```bash
|
||||
cd hello
|
||||
go run main.go
|
||||
```
|
||||
|
||||
### [crud](./crud/) - CRUD Contact Book
|
||||
|
||||
A realistic service with create, read, update, delete, list, and search operations. Shows how to document a full API for agents with `@example` tags, `description` struct tags, validation errors, and partial updates.
|
||||
|
||||
**Run it:**
|
||||
```bash
|
||||
cd crud
|
||||
go run main.go
|
||||
```
|
||||
|
||||
### [workflow](./workflow/) - Cross-Service Orchestration
|
||||
|
||||
Three services (Inventory, Orders, Notifications) showing how an AI agent orchestrates multi-step workflows: search products, check stock, reserve inventory, place order, send confirmation — all from a single natural language request.
|
||||
|
||||
**Run it:**
|
||||
```bash
|
||||
cd workflow
|
||||
go run main.go
|
||||
```
|
||||
|
||||
### [platform](./platform/) - Agent Platform Showcase
|
||||
|
||||
A complete platform (Users, Posts, Comments, Mail) mirroring [micro/blog](https://github.com/micro/blog). Shows how existing microservices become agent-accessible with zero code changes — agents can sign up, write posts, comment, tag, and send mail through natural language.
|
||||
|
||||
**Run it:**
|
||||
```bash
|
||||
cd platform
|
||||
go run main.go
|
||||
```
|
||||
|
||||
### [documented](./documented/) - Full-Featured Example
|
||||
|
||||
Complete example showing all MCP features with a user service.
|
||||
|
||||
**What it shows:**
|
||||
- Multiple endpoints (GetUser, CreateUser)
|
||||
- Rich documentation with examples
|
||||
- Per-endpoint auth scopes via `server.WithEndpointScopes()`
|
||||
- Pre-populated test data
|
||||
- Production-ready patterns
|
||||
|
||||
**Run it:**
|
||||
```bash
|
||||
cd documented
|
||||
go run main.go
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Write Your Service
|
||||
|
||||
Add Go doc comments to your handler methods:
|
||||
|
||||
```go
|
||||
// SayHello greets a person by name. Returns a friendly greeting message.
|
||||
//
|
||||
// @example {"name": "Alice"}
|
||||
func (g *Greeter) SayHello(ctx context.Context, req *HelloRequest, rsp *HelloResponse) error {
|
||||
rsp.Message = "Hello " + req.Name + "!"
|
||||
return nil
|
||||
}
|
||||
|
||||
type HelloRequest struct {
|
||||
Name string `json:"name" description:"Person's name to greet"`
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Register Handler (Auto-Extracts Docs!)
|
||||
|
||||
```go
|
||||
handler := service.Server().NewHandler(new(Greeter))
|
||||
service.Server().Handle(handler)
|
||||
```
|
||||
|
||||
### 3. Start MCP Gateway
|
||||
|
||||
```go
|
||||
go mcp.ListenAndServe(":3000", mcp.Options{
|
||||
Registry: service.Options().Registry,
|
||||
})
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### HTTP API
|
||||
|
||||
```bash
|
||||
# List tools
|
||||
curl http://localhost:3000/mcp/tools | jq
|
||||
|
||||
# Call a tool
|
||||
curl -X POST http://localhost:3000/mcp/call \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"tool": "greeter.Greeter.SayHello",
|
||||
"input": {"name": "Alice"}
|
||||
}' | jq
|
||||
```
|
||||
|
||||
### Claude Code (Stdio)
|
||||
|
||||
Start MCP server:
|
||||
```bash
|
||||
micro mcp serve
|
||||
```
|
||||
|
||||
Add to `~/.claude/claude_desktop_config.json`:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-services": {
|
||||
"command": "micro",
|
||||
"args": ["mcp", "serve"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Restart Claude Code and ask Claude to use your services!
|
||||
|
||||
## Features
|
||||
|
||||
### ✅ Automatic Documentation Extraction
|
||||
|
||||
Just write Go comments - documentation is extracted automatically:
|
||||
|
||||
- **Go doc comments** → Tool descriptions
|
||||
- **@example tags** → Example inputs for AI
|
||||
- **Struct tags** → Parameter descriptions
|
||||
|
||||
### ✅ Multiple Transports
|
||||
|
||||
- **Stdio** - For Claude Code (recommended)
|
||||
- **HTTP/SSE** - For web-based agents
|
||||
|
||||
### ✅ MCP Command Line
|
||||
|
||||
```bash
|
||||
# Start MCP server
|
||||
micro mcp serve # Stdio (for Claude Code)
|
||||
micro mcp serve --address :3000 # HTTP/SSE (for web agents)
|
||||
|
||||
# List available tools
|
||||
micro mcp list # Human-readable list
|
||||
micro mcp list --json # JSON output
|
||||
|
||||
# Test a tool
|
||||
micro mcp test <tool-name> '{"key": "value"}'
|
||||
|
||||
# Generate documentation
|
||||
micro mcp docs # Markdown format
|
||||
micro mcp docs --format json # JSON format
|
||||
micro mcp docs --output tools.md # Save to file
|
||||
|
||||
# Export to different formats
|
||||
micro mcp export langchain # Python LangChain tools
|
||||
micro mcp export openapi # OpenAPI 3.0 spec
|
||||
micro mcp export json # Raw JSON definitions
|
||||
```
|
||||
|
||||
For detailed examples, see [CLI Examples](../../cmd/micro/mcp/EXAMPLES.md).
|
||||
|
||||
### ✅ Zero Configuration
|
||||
|
||||
- No manual tool registration
|
||||
- No API wrappers
|
||||
- No code generation
|
||||
- Just write normal Go code!
|
||||
|
||||
### ✅ Per-Tool Auth Scopes
|
||||
|
||||
Declare required scopes when registering a handler:
|
||||
|
||||
```go
|
||||
handler := service.Server().NewHandler(
|
||||
new(BlogService),
|
||||
server.WithEndpointScopes("Blog.Create", "blog:write"),
|
||||
server.WithEndpointScopes("Blog.Delete", "blog:admin"),
|
||||
)
|
||||
```
|
||||
|
||||
Or define scopes at the gateway layer without changing services:
|
||||
|
||||
```go
|
||||
mcp.Serve(mcp.Options{
|
||||
Registry: reg,
|
||||
Auth: authProvider,
|
||||
Scopes: map[string][]string{
|
||||
"blog.Blog.Create": {"blog:write"},
|
||||
"blog.Blog.Delete": {"blog:admin"},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### ✅ Tracing, Rate Limiting & Audit Logging
|
||||
|
||||
Every tool call generates a trace ID that propagates through the RPC chain.
|
||||
Configure rate limiting and audit logging at the gateway:
|
||||
|
||||
```go
|
||||
mcp.Serve(mcp.Options{
|
||||
Registry: reg,
|
||||
Auth: authProvider,
|
||||
RateLimit: &mcp.RateLimitConfig{
|
||||
RequestsPerSecond: 10,
|
||||
Burst: 20,
|
||||
},
|
||||
AuditFunc: func(r mcp.AuditRecord) {
|
||||
log.Printf("[audit] trace=%s tool=%s account=%s allowed=%v",
|
||||
r.TraceID, r.Tool, r.AccountID, r.Allowed)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Full MCP Documentation](../../internal/website/docs/mcp.md)
|
||||
- [MCP Gateway Implementation](../../gateway/mcp/)
|
||||
- [Documentation Guide](../../gateway/mcp/DOCUMENTATION.md)
|
||||
- [Blog Post](../../internal/website/blog/2.md)
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Model Context Protocol Spec](https://modelcontextprotocol.io/)
|
||||
- [Go Micro Documentation](https://go-micro.dev)
|
||||
@@ -0,0 +1,74 @@
|
||||
# CRUD Contact Book Example
|
||||
|
||||
A complete CRUD service with MCP integration — the kind of service you'd actually build in production.
|
||||
|
||||
## What This Shows
|
||||
|
||||
- **6 operations**: Create, Get, Update, Delete, List, Search
|
||||
- **Rich documentation**: Every handler has doc comments with `@example` tags
|
||||
- **Struct tag descriptions**: All fields have `description` tags for agents
|
||||
- **Input validation**: Required field checks with clear error messages
|
||||
- **Partial updates**: Update only changes non-empty fields
|
||||
- **Seed data**: Starts with 3 contacts so agents can explore immediately
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
go run .
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
# List all MCP tools
|
||||
curl http://localhost:3001/mcp/tools | jq
|
||||
|
||||
# Create a contact
|
||||
curl -X POST http://localhost:3001/mcp/call \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"tool": "contacts.Contacts.Create", "arguments": {"name": "Dave", "email": "dave@example.com"}}'
|
||||
|
||||
# Search contacts
|
||||
curl -X POST http://localhost:3001/mcp/call \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"tool": "contacts.Contacts.Search", "arguments": {"query": "engineer"}}'
|
||||
```
|
||||
|
||||
## Use with Claude Code
|
||||
|
||||
```bash
|
||||
micro mcp serve
|
||||
```
|
||||
|
||||
Then ask: "List all contacts and find the engineers."
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### Doc Comments for Agents
|
||||
|
||||
```go
|
||||
// Create adds a new contact to the book. Name and email are required.
|
||||
//
|
||||
// @example {"name": "Dave Wilson", "email": "dave@example.com", "role": "Engineer"}
|
||||
func (h *Contacts) Create(ctx context.Context, req *CreateRequest, rsp *CreateResponse) error {
|
||||
```
|
||||
|
||||
### Struct Tag Descriptions
|
||||
|
||||
```go
|
||||
type Contact struct {
|
||||
ID string `json:"id" description:"Unique contact identifier"`
|
||||
Name string `json:"name" description:"Full name"`
|
||||
Email string `json:"email" description:"Email address"`
|
||||
}
|
||||
```
|
||||
|
||||
### Partial Updates
|
||||
|
||||
Only update fields that are provided (non-empty), so agents can change one field without overwriting others:
|
||||
|
||||
```go
|
||||
if req.Name != "" {
|
||||
contact.Name = req.Name
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,278 @@
|
||||
// CRUD example: a contact book service with full MCP integration.
|
||||
//
|
||||
// This shows a realistic service with create, read, update, delete, and
|
||||
// search operations, all automatically exposed as MCP tools with rich
|
||||
// documentation for AI agents.
|
||||
//
|
||||
// Run:
|
||||
//
|
||||
// go run .
|
||||
//
|
||||
// MCP tools: http://localhost:3001/mcp/tools
|
||||
// Test: curl http://localhost:3001/mcp/tools | jq
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
"go-micro.dev/v6/gateway/mcp"
|
||||
)
|
||||
|
||||
// --- Types ---
|
||||
|
||||
// Contact represents a person in the contact book.
|
||||
type Contact struct {
|
||||
ID string `json:"id" description:"Unique contact identifier"`
|
||||
Name string `json:"name" description:"Full name"`
|
||||
Email string `json:"email" description:"Email address"`
|
||||
Phone string `json:"phone" description:"Phone number in E.164 format"`
|
||||
Role string `json:"role" description:"Job title or role"`
|
||||
Notes string `json:"notes" description:"Free-text notes about this contact"`
|
||||
}
|
||||
|
||||
type CreateRequest struct {
|
||||
Name string `json:"name" description:"Full name (required)"`
|
||||
Email string `json:"email" description:"Email address (required)"`
|
||||
Phone string `json:"phone" description:"Phone number"`
|
||||
Role string `json:"role" description:"Job title or role"`
|
||||
Notes string `json:"notes" description:"Free-text notes"`
|
||||
}
|
||||
|
||||
type CreateResponse struct {
|
||||
Contact *Contact `json:"contact" description:"The newly created contact"`
|
||||
}
|
||||
|
||||
type GetRequest struct {
|
||||
ID string `json:"id" description:"Contact ID to look up"`
|
||||
}
|
||||
|
||||
type GetResponse struct {
|
||||
Contact *Contact `json:"contact" description:"The requested contact"`
|
||||
}
|
||||
|
||||
type UpdateRequest struct {
|
||||
ID string `json:"id" description:"Contact ID to update (required)"`
|
||||
Name string `json:"name" description:"New name (leave empty to keep current)"`
|
||||
Email string `json:"email" description:"New email (leave empty to keep current)"`
|
||||
Phone string `json:"phone" description:"New phone (leave empty to keep current)"`
|
||||
Role string `json:"role" description:"New role (leave empty to keep current)"`
|
||||
Notes string `json:"notes" description:"New notes (leave empty to keep current)"`
|
||||
}
|
||||
|
||||
type UpdateResponse struct {
|
||||
Contact *Contact `json:"contact" description:"The updated contact"`
|
||||
}
|
||||
|
||||
type DeleteRequest struct {
|
||||
ID string `json:"id" description:"Contact ID to delete"`
|
||||
}
|
||||
|
||||
type DeleteResponse struct {
|
||||
Deleted bool `json:"deleted" description:"True if the contact was deleted"`
|
||||
}
|
||||
|
||||
type ListRequest struct {
|
||||
}
|
||||
|
||||
type ListResponse struct {
|
||||
Contacts []*Contact `json:"contacts" description:"All contacts in the book"`
|
||||
}
|
||||
|
||||
type SearchRequest struct {
|
||||
Query string `json:"query" description:"Search term to match against name, email, role, or notes"`
|
||||
}
|
||||
|
||||
type SearchResponse struct {
|
||||
Contacts []*Contact `json:"contacts" description:"Contacts matching the search query"`
|
||||
}
|
||||
|
||||
// --- Handler ---
|
||||
|
||||
// Contacts manages a contact book with CRUD operations.
|
||||
type Contacts struct {
|
||||
mu sync.RWMutex
|
||||
store map[string]*Contact
|
||||
counter int
|
||||
}
|
||||
|
||||
func NewContacts() *Contacts {
|
||||
c := &Contacts{store: make(map[string]*Contact)}
|
||||
// Seed with example data
|
||||
c.store["c-1"] = &Contact{ID: "c-1", Name: "Alice Johnson", Email: "alice@example.com", Phone: "+1-555-0101", Role: "Engineer", Notes: "Backend team lead"}
|
||||
c.store["c-2"] = &Contact{ID: "c-2", Name: "Bob Smith", Email: "bob@example.com", Phone: "+1-555-0102", Role: "Designer", Notes: "UI/UX specialist"}
|
||||
c.store["c-3"] = &Contact{ID: "c-3", Name: "Carol Davis", Email: "carol@example.com", Phone: "+1-555-0103", Role: "PM", Notes: "Leads the platform team"}
|
||||
c.counter = 3
|
||||
return c
|
||||
}
|
||||
|
||||
// Create adds a new contact to the book. Name and email are required.
|
||||
//
|
||||
// @example {"name": "Dave Wilson", "email": "dave@example.com", "role": "Engineer"}
|
||||
func (h *Contacts) Create(ctx context.Context, req *CreateRequest, rsp *CreateResponse) error {
|
||||
if req.Name == "" {
|
||||
return fmt.Errorf("name is required")
|
||||
}
|
||||
if req.Email == "" {
|
||||
return fmt.Errorf("email is required")
|
||||
}
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
h.counter++
|
||||
id := fmt.Sprintf("c-%d", h.counter)
|
||||
contact := &Contact{
|
||||
ID: id,
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
Phone: req.Phone,
|
||||
Role: req.Role,
|
||||
Notes: req.Notes,
|
||||
}
|
||||
h.store[id] = contact
|
||||
rsp.Contact = contact
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get retrieves a single contact by ID.
|
||||
//
|
||||
// @example {"id": "c-1"}
|
||||
func (h *Contacts) Get(ctx context.Context, req *GetRequest, rsp *GetResponse) error {
|
||||
if req.ID == "" {
|
||||
return fmt.Errorf("id is required")
|
||||
}
|
||||
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
contact, ok := h.store[req.ID]
|
||||
if !ok {
|
||||
return fmt.Errorf("contact %s not found", req.ID)
|
||||
}
|
||||
rsp.Contact = contact
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update modifies an existing contact. Only non-empty fields are updated,
|
||||
// so you can change just the email without affecting other fields.
|
||||
//
|
||||
// @example {"id": "c-1", "role": "Senior Engineer"}
|
||||
func (h *Contacts) Update(ctx context.Context, req *UpdateRequest, rsp *UpdateResponse) error {
|
||||
if req.ID == "" {
|
||||
return fmt.Errorf("id is required")
|
||||
}
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
contact, ok := h.store[req.ID]
|
||||
if !ok {
|
||||
return fmt.Errorf("contact %s not found", req.ID)
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
contact.Name = req.Name
|
||||
}
|
||||
if req.Email != "" {
|
||||
contact.Email = req.Email
|
||||
}
|
||||
if req.Phone != "" {
|
||||
contact.Phone = req.Phone
|
||||
}
|
||||
if req.Role != "" {
|
||||
contact.Role = req.Role
|
||||
}
|
||||
if req.Notes != "" {
|
||||
contact.Notes = req.Notes
|
||||
}
|
||||
|
||||
rsp.Contact = contact
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes a contact from the book permanently.
|
||||
//
|
||||
// @example {"id": "c-1"}
|
||||
func (h *Contacts) Delete(ctx context.Context, req *DeleteRequest, rsp *DeleteResponse) error {
|
||||
if req.ID == "" {
|
||||
return fmt.Errorf("id is required")
|
||||
}
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if _, ok := h.store[req.ID]; !ok {
|
||||
return fmt.Errorf("contact %s not found", req.ID)
|
||||
}
|
||||
|
||||
delete(h.store, req.ID)
|
||||
rsp.Deleted = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns all contacts in the book.
|
||||
//
|
||||
// @example {}
|
||||
func (h *Contacts) List(ctx context.Context, req *ListRequest, rsp *ListResponse) error {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
for _, c := range h.store {
|
||||
rsp.Contacts = append(rsp.Contacts, c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Search finds contacts matching a query string. Matches against name,
|
||||
// email, role, and notes fields (case-insensitive).
|
||||
//
|
||||
// @example {"query": "engineer"}
|
||||
func (h *Contacts) Search(ctx context.Context, req *SearchRequest, rsp *SearchResponse) error {
|
||||
if req.Query == "" {
|
||||
return fmt.Errorf("query is required")
|
||||
}
|
||||
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
q := strings.ToLower(req.Query)
|
||||
for _, c := range h.store {
|
||||
if strings.Contains(strings.ToLower(c.Name), q) ||
|
||||
strings.Contains(strings.ToLower(c.Email), q) ||
|
||||
strings.Contains(strings.ToLower(c.Role), q) ||
|
||||
strings.Contains(strings.ToLower(c.Notes), q) {
|
||||
rsp.Contacts = append(rsp.Contacts, c)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
service := micro.NewService("contacts",
|
||||
micro.Address(":9010"),
|
||||
mcp.WithMCP(":3001"),
|
||||
)
|
||||
service.Init()
|
||||
|
||||
if err := service.Handle(NewContacts()); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println("Contacts service running on :9010")
|
||||
fmt.Println("MCP tools available at http://localhost:3001/mcp/tools")
|
||||
fmt.Println()
|
||||
fmt.Println("Try asking an AI agent:")
|
||||
fmt.Println(" 'List all contacts'")
|
||||
fmt.Println(" 'Find engineers in the contact book'")
|
||||
fmt.Println(" 'Add a new contact for Eve at eve@example.com'")
|
||||
fmt.Println(" 'Update Alice's role to Staff Engineer'")
|
||||
|
||||
if err := service.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
# Documented Service Example
|
||||
|
||||
This example demonstrates how to document your go-micro service handlers so that AI agents can understand them better. The MCP gateway parses Go comments and struct tags to generate rich tool descriptions.
|
||||
|
||||
## Documentation Features
|
||||
|
||||
### 1. **Go Doc Comments**
|
||||
|
||||
Standard Go documentation comments are used as the tool description:
|
||||
|
||||
```go
|
||||
// GetUser retrieves a user by ID from the database.
|
||||
//
|
||||
// This endpoint fetches a user's complete profile including their name,
|
||||
// email, and age. If the user doesn't exist, an error is returned.
|
||||
func (u *Users) GetUser(ctx context.Context, req *GetUserRequest, rsp *GetUserResponse) error {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **JSDoc-Style Tags**
|
||||
|
||||
Use `@param`, `@return`, and `@example` tags for detailed documentation:
|
||||
|
||||
```go
|
||||
// CreateUser creates a new user in the system.
|
||||
//
|
||||
// @param name {string} User's full name (required, 1-100 characters)
|
||||
// @param email {string} User's email address (required, must be valid email format)
|
||||
// @param age {number} User's age (optional, must be 0-150 if provided)
|
||||
// @return {User} The newly created user with generated ID
|
||||
// @example
|
||||
// {
|
||||
// "name": "Alice Smith",
|
||||
// "email": "alice@example.com",
|
||||
// "age": 30
|
||||
// }
|
||||
func (u *Users) CreateUser(ctx context.Context, req *CreateUserRequest, rsp *CreateUserResponse) error {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 3. **Struct Tags**
|
||||
|
||||
Add `description` tags to struct fields for better schema:
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
ID string `json:"id" description:"User's unique identifier (UUID format)"`
|
||||
Name string `json:"name" description:"User's full name"`
|
||||
Email string `json:"email" description:"User's email address"`
|
||||
Age int `json:"age,omitempty" description:"User's age (optional)"`
|
||||
}
|
||||
```
|
||||
|
||||
## Running the Example
|
||||
|
||||
### 1. Start the Service
|
||||
|
||||
```bash
|
||||
cd examples/mcp/documented
|
||||
go run main.go
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
Users service starting...
|
||||
Service: users
|
||||
Endpoints:
|
||||
- Users.GetUser
|
||||
- Users.CreateUser
|
||||
MCP Gateway: http://localhost:3000
|
||||
```
|
||||
|
||||
### 2. Test MCP Tools
|
||||
|
||||
List available tools:
|
||||
```bash
|
||||
curl http://localhost:3000/mcp/tools | jq
|
||||
```
|
||||
|
||||
You'll see rich descriptions:
|
||||
|
||||
```json
|
||||
{
|
||||
"tools": [
|
||||
{
|
||||
"name": "users.Users.GetUser",
|
||||
"description": "GetUser retrieves a user by ID from the database",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "User ID in UUID format (e.g., \"123e4567-e89b-12d3-a456-426614174000\")"
|
||||
}
|
||||
},
|
||||
"required": ["id"],
|
||||
"examples": [
|
||||
"{\"id\": \"123e4567-e89b-12d3-a456-426614174000\"}"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "users.Users.CreateUser",
|
||||
"description": "CreateUser creates a new user in the system",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "User's full name (required, 1-100 characters)"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "User's email address (required, must be valid email format)"
|
||||
},
|
||||
"age": {
|
||||
"type": "number",
|
||||
"description": "User's age (optional, must be 0-150 if provided)"
|
||||
}
|
||||
},
|
||||
"required": ["name", "email"],
|
||||
"examples": [
|
||||
"{\"name\": \"Alice Smith\", \"email\": \"alice@example.com\", \"age\": 30}"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Call a Tool
|
||||
|
||||
Get existing user:
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/mcp/call \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"tool": "users.Users.GetUser",
|
||||
"input": {"id": "user-1"}
|
||||
}'
|
||||
```
|
||||
|
||||
Create new user:
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/mcp/call \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"tool": "users.Users.CreateUser",
|
||||
"input": {
|
||||
"name": "Alice Smith",
|
||||
"email": "alice@example.com",
|
||||
"age": 30
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### 4. Use with Claude Code
|
||||
|
||||
Add to your `claude_desktop_config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"users-service": {
|
||||
"command": "go",
|
||||
"args": ["run", "/path/to/examples/mcp/documented/main.go"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then in Claude Code, ask:
|
||||
```
|
||||
> You: "Show me user-1's profile"
|
||||
|
||||
Claude will:
|
||||
1. See the GetUser tool with rich description
|
||||
2. Understand it needs an "id" parameter (UUID format)
|
||||
3. Call users.Users.GetUser with {"id": "user-1"}
|
||||
4. Return the user profile
|
||||
```
|
||||
|
||||
## Documentation Best Practices
|
||||
|
||||
### DO: Write Clear Descriptions
|
||||
|
||||
```go
|
||||
// ✅ Good: Clear, explains what and why
|
||||
// GetUser retrieves a user by ID from the database.
|
||||
// Returns full profile including email, name, and preferences.
|
||||
```
|
||||
|
||||
```go
|
||||
// ❌ Bad: Vague, no context
|
||||
// Get gets a user
|
||||
```
|
||||
|
||||
### DO: Specify Parameter Constraints
|
||||
|
||||
```go
|
||||
// ✅ Good: Specifies format and constraints
|
||||
// @param id {string} User ID in UUID format (e.g., "123e4567-e89b-12d3-a456-426614174000")
|
||||
// @param age {number} User's age (must be 0-150)
|
||||
```
|
||||
|
||||
```go
|
||||
// ❌ Bad: No constraints or format
|
||||
// @param id {string} The ID
|
||||
```
|
||||
|
||||
### DO: Provide Examples
|
||||
|
||||
```go
|
||||
// ✅ Good: Real example agents can use
|
||||
// @example
|
||||
// {
|
||||
// "name": "Alice Smith",
|
||||
// "email": "alice@example.com",
|
||||
// "age": 30
|
||||
// }
|
||||
```
|
||||
|
||||
```go
|
||||
// ❌ Bad: No example
|
||||
// (agents have to guess the format)
|
||||
```
|
||||
|
||||
### DO: Use Descriptive Struct Tags
|
||||
|
||||
```go
|
||||
// ✅ Good: Explains what the field is
|
||||
type User struct {
|
||||
ID string `json:"id" description:"User's unique identifier (UUID format)"`
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// ❌ Bad: No description
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Go Doc Parsing**
|
||||
- The MCP gateway reads your service's Go comments
|
||||
- First line becomes the tool description
|
||||
- Full comment becomes the detailed description
|
||||
|
||||
2. **JSDoc Tag Parsing**
|
||||
- `@param` tags enhance parameter descriptions
|
||||
- `@return` tags describe what the tool returns
|
||||
- `@example` tags provide usage examples
|
||||
|
||||
3. **Struct Tag Reading**
|
||||
- `description` tags add context to fields
|
||||
- `json:"field,omitempty"` marks optional fields
|
||||
- Used to generate JSON Schema for parameters
|
||||
|
||||
4. **Schema Generation**
|
||||
- Combines parsed documentation with type information
|
||||
- Creates rich JSON Schema for each tool
|
||||
- Agents use this to understand how to call your service
|
||||
|
||||
## Impact on Agent Performance
|
||||
|
||||
### Without Documentation
|
||||
|
||||
```
|
||||
Tool: users.Users.GetUser
|
||||
Description: Call GetUser on users service
|
||||
Parameters: { "id": "string" }
|
||||
```
|
||||
|
||||
Agent thinks: *"What's an ID? What format? What if I pass the wrong thing?"*
|
||||
|
||||
### With Documentation
|
||||
|
||||
```
|
||||
Tool: users.Users.GetUser
|
||||
Description: Retrieves a user by ID from the database. Returns full profile
|
||||
including email, name, and preferences.
|
||||
Parameters:
|
||||
- id (string, required): User ID in UUID format
|
||||
Example: "123e4567-e89b-12d3-a456-426614174000"
|
||||
Example:
|
||||
{"id": "user-1"}
|
||||
```
|
||||
|
||||
Agent thinks: *"I need a UUID format ID. I can use 'user-1' from the example!"*
|
||||
|
||||
**Result:** Agent calls your service correctly on the first try!
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Document all your service handlers with clear descriptions
|
||||
- Add `@param`, `@return`, and `@example` tags
|
||||
- Use `description` tags in struct fields
|
||||
- Test with Claude Code to see how agents understand your services
|
||||
|
||||
## License
|
||||
|
||||
Apache 2.0
|
||||
@@ -0,0 +1,137 @@
|
||||
// Package main demonstrates how to document your service handlers for better
|
||||
// AI agent integration using endpoint metadata.
|
||||
//
|
||||
// Services register descriptions with their endpoints, and the MCP gateway
|
||||
// reads these descriptions from the registry to generate rich tool descriptions.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
"go-micro.dev/v6/gateway/mcp"
|
||||
"go-micro.dev/v6/server"
|
||||
)
|
||||
|
||||
// User represents a user in the system
|
||||
type User struct {
|
||||
ID string `json:"id" description:"User's unique identifier (UUID format)"`
|
||||
Name string `json:"name" description:"User's full name"`
|
||||
Email string `json:"email" description:"User's email address"`
|
||||
Age int `json:"age,omitempty" description:"User's age (optional)"`
|
||||
}
|
||||
|
||||
// GetUserRequest is the request for getting a user
|
||||
type GetUserRequest struct {
|
||||
ID string `json:"id" description:"User ID to retrieve"`
|
||||
}
|
||||
|
||||
// GetUserResponse is the response containing user data
|
||||
type GetUserResponse struct {
|
||||
User *User `json:"user" description:"The requested user object"`
|
||||
}
|
||||
|
||||
// CreateUserRequest is the request for creating a user
|
||||
type CreateUserRequest struct {
|
||||
Name string `json:"name" description:"User's full name (required)"`
|
||||
Email string `json:"email" description:"User's email address (required)"`
|
||||
Age int `json:"age,omitempty" description:"User's age (optional)"`
|
||||
}
|
||||
|
||||
// CreateUserResponse contains the newly created user
|
||||
type CreateUserResponse struct {
|
||||
User *User `json:"user" description:"The newly created user"`
|
||||
}
|
||||
|
||||
// Users service handles user-related operations
|
||||
type Users struct {
|
||||
users map[string]*User
|
||||
}
|
||||
|
||||
// GetUser retrieves a user by ID from the database. Returns full profile including email, name, and preferences. If the user doesn't exist, an error is returned.
|
||||
//
|
||||
// @example {"id": "user-1"}
|
||||
func (u *Users) GetUser(ctx context.Context, req *GetUserRequest, rsp *GetUserResponse) error {
|
||||
user, exists := u.users[req.ID]
|
||||
if !exists {
|
||||
return fmt.Errorf("user not found: %s", req.ID)
|
||||
}
|
||||
|
||||
rsp.User = user
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateUser creates a new user in the system. Validates the user data and creates a new profile. Name and email are required fields, while age is optional. Email must be unique across all users.
|
||||
//
|
||||
// @example {"name": "Alice Smith", "email": "alice@example.com", "age": 30}
|
||||
func (u *Users) CreateUser(ctx context.Context, req *CreateUserRequest, rsp *CreateUserResponse) error {
|
||||
// Validate input
|
||||
if req.Name == "" || req.Email == "" {
|
||||
return fmt.Errorf("name and email are required")
|
||||
}
|
||||
|
||||
// Generate ID (simplified for example)
|
||||
id := fmt.Sprintf("user-%d", len(u.users)+1)
|
||||
|
||||
user := &User{
|
||||
ID: id,
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
Age: req.Age,
|
||||
}
|
||||
|
||||
u.users[id] = user
|
||||
rsp.User = user
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Create service
|
||||
service := micro.NewService("users",
|
||||
micro.Address(":9090"),
|
||||
// Start MCP gateway alongside the service
|
||||
mcp.WithMCP(":3000"),
|
||||
)
|
||||
|
||||
service.Init()
|
||||
|
||||
// Register handler with pre-populated test data.
|
||||
// Documentation is automatically extracted from method comments.
|
||||
// Use WithEndpointScopes to declare required auth scopes per endpoint.
|
||||
if err := service.Handle(
|
||||
&Users{
|
||||
users: map[string]*User{
|
||||
"user-1": {ID: "user-1", Name: "John Doe", Email: "john@example.com", Age: 25},
|
||||
"user-2": {ID: "user-2", Name: "Jane Smith", Email: "jane@example.com", Age: 30},
|
||||
},
|
||||
},
|
||||
server.WithEndpointScopes("Users.GetUser", "users:read"),
|
||||
server.WithEndpointScopes("Users.CreateUser", "users:write"),
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Println("Users service starting...")
|
||||
log.Println("Service: users")
|
||||
log.Println("Endpoints:")
|
||||
log.Println(" - Users.GetUser")
|
||||
log.Println(" - Users.CreateUser")
|
||||
log.Println("MCP Gateway: http://localhost:3000")
|
||||
log.Println("")
|
||||
log.Println("Test with:")
|
||||
log.Println(" curl http://localhost:3000/mcp/tools")
|
||||
log.Println("")
|
||||
log.Println("Or add to Claude Code:")
|
||||
log.Println(` "users-service": {`)
|
||||
log.Println(` "command": "micro",`)
|
||||
log.Println(` "args": ["mcp", "serve"]`)
|
||||
log.Println(` }`)
|
||||
|
||||
// Run service
|
||||
if err := service.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
# MCP Hello World Example
|
||||
|
||||
The simplest possible MCP-enabled go-micro service.
|
||||
|
||||
## What This Shows
|
||||
|
||||
- ✅ Automatic documentation extraction from Go comments
|
||||
- ✅ MCP gateway setup with 3 lines of code
|
||||
- ✅ Ready for Claude Code integration
|
||||
- ✅ HTTP endpoint for testing
|
||||
|
||||
## Run It
|
||||
|
||||
```bash
|
||||
cd examples/mcp/hello
|
||||
go run main.go
|
||||
```
|
||||
|
||||
## Test It
|
||||
|
||||
### Option 1: HTTP API
|
||||
|
||||
```bash
|
||||
# List available tools
|
||||
curl http://localhost:3000/mcp/tools | jq
|
||||
|
||||
# Call the SayHello tool
|
||||
curl -X POST http://localhost:3000/mcp/call \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"tool": "greeter.Greeter.SayHello",
|
||||
"input": {"name": "Alice"}
|
||||
}' | jq
|
||||
```
|
||||
|
||||
### Option 2: Claude Code
|
||||
|
||||
In a separate terminal:
|
||||
|
||||
```bash
|
||||
micro mcp serve
|
||||
```
|
||||
|
||||
Add to `~/.claude/claude_desktop_config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"greeter": {
|
||||
"command": "micro",
|
||||
"args": ["mcp", "serve"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Restart Claude Code and ask:
|
||||
|
||||
> "Say hello to Bob using the greeter service"
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. Write Normal Go Code
|
||||
|
||||
```go
|
||||
// SayHello greets a person by name. Returns a friendly greeting message.
|
||||
//
|
||||
// @example {"name": "Alice"}
|
||||
func (g *Greeter) SayHello(ctx context.Context, req *HelloRequest, rsp *HelloResponse) error {
|
||||
rsp.Message = "Hello " + req.Name + "!"
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Register the Handler
|
||||
|
||||
```go
|
||||
// Documentation is extracted automatically!
|
||||
handler := service.Server().NewHandler(new(Greeter))
|
||||
service.Server().Handle(handler)
|
||||
```
|
||||
|
||||
### 3. Start MCP Gateway
|
||||
|
||||
```go
|
||||
go mcp.ListenAndServe(":3000", mcp.Options{
|
||||
Registry: service.Options().Registry,
|
||||
})
|
||||
```
|
||||
|
||||
**That's it!** Your service is now AI-accessible.
|
||||
|
||||
## What Gets Extracted
|
||||
|
||||
From this code:
|
||||
|
||||
```go
|
||||
// SayHello greets a person by name. Returns a friendly greeting message.
|
||||
//
|
||||
// @example {"name": "Alice"}
|
||||
func (g *Greeter) SayHello(...)
|
||||
|
||||
type HelloRequest struct {
|
||||
Name string `json:"name" description:"Person's name to greet"`
|
||||
}
|
||||
```
|
||||
|
||||
Claude sees:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "greeter.Greeter.SayHello",
|
||||
"description": "SayHello greets a person by name. Returns a friendly greeting message.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Person's name to greet"
|
||||
}
|
||||
},
|
||||
"examples": ["{\"name\": \"Alice\"}"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- See `examples/mcp/documented` for a more complete example with multiple endpoints
|
||||
- Read `/docs/mcp.md` for full documentation
|
||||
- Check out the [MCP specification](https://modelcontextprotocol.io/)
|
||||
@@ -0,0 +1,65 @@
|
||||
// Package main demonstrates a minimal MCP-enabled service.
|
||||
//
|
||||
// This is the simplest possible example showing:
|
||||
// - Automatic documentation extraction from Go comments
|
||||
// - MCP gateway setup
|
||||
// - Ready for use with Claude Code
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
"go-micro.dev/v6/gateway/mcp"
|
||||
)
|
||||
|
||||
// Greeter service handles greeting operations
|
||||
type Greeter struct{}
|
||||
|
||||
// SayHello greets a person by name. Returns a friendly greeting message.
|
||||
//
|
||||
// @example {"name": "Alice"}
|
||||
func (g *Greeter) SayHello(ctx context.Context, req *HelloRequest, rsp *HelloResponse) error {
|
||||
rsp.Message = "Hello " + req.Name + "!"
|
||||
return nil
|
||||
}
|
||||
|
||||
// HelloRequest contains the greeting parameters
|
||||
type HelloRequest struct {
|
||||
Name string `json:"name" description:"Person's name to greet"`
|
||||
}
|
||||
|
||||
// HelloResponse contains the greeting result
|
||||
type HelloResponse struct {
|
||||
Message string `json:"message" description:"The greeting message"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Create service
|
||||
service := micro.NewService("greeter",
|
||||
micro.Address(":9090"),
|
||||
// Start MCP gateway alongside the service
|
||||
mcp.WithMCP(":3000"),
|
||||
)
|
||||
|
||||
service.Init()
|
||||
|
||||
// Register handler — docs extracted automatically from comments
|
||||
if err := service.Handle(new(Greeter)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Println("Greeter service starting...")
|
||||
log.Println("Service: http://localhost:9090")
|
||||
log.Println("MCP Gateway: http://localhost:3000")
|
||||
log.Println("MCP Tools: http://localhost:3000/mcp/tools")
|
||||
log.Println()
|
||||
log.Println("Use with Claude Code:")
|
||||
log.Println(" micro mcp serve")
|
||||
|
||||
// Run service
|
||||
if err := service.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
# Platform Example: AI Agents Meet Real Microservices
|
||||
|
||||
This example mirrors the [micro/blog](https://github.com/micro/blog) platform — a real microblogging application built on Go Micro. It demonstrates how existing microservices become AI-accessible through MCP with **zero changes to business logic**.
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Endpoints | Description |
|
||||
|---------|-----------|-------------|
|
||||
| **Users** | Signup, Login, GetProfile, UpdateStatus, List | Account management and authentication |
|
||||
| **Posts** | Create, Read, Update, Delete, List, TagPost, UntagPost, ListTags | Blog posts with markdown and tagging |
|
||||
| **Comments** | Create, List, Delete | Threaded comments on posts |
|
||||
| **Mail** | Send, Read | Internal messaging between users |
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
go run .
|
||||
```
|
||||
|
||||
MCP tools available at: http://localhost:3001/mcp/tools
|
||||
|
||||
## Agent Scenarios
|
||||
|
||||
These are realistic multi-step workflows an AI agent can complete:
|
||||
|
||||
### 1. New User Onboarding
|
||||
```
|
||||
"Sign up a new user called carol, then write a welcome post introducing herself"
|
||||
```
|
||||
The agent will: call Signup → use the returned user ID → call Posts.Create
|
||||
|
||||
### 2. Content Creation
|
||||
```
|
||||
"Log in as alice and write a blog post about Go concurrency patterns, then tag it with 'golang' and 'concurrency'"
|
||||
```
|
||||
The agent will: call Login → call Posts.Create → call TagPost twice
|
||||
|
||||
### 3. Social Interaction
|
||||
```
|
||||
"List all posts, find the welcome post, and comment on it as bob saying 'Great to be here!'"
|
||||
```
|
||||
The agent will: call Posts.List → pick the right post → call Comments.Create
|
||||
|
||||
### 4. Cross-Service Workflow
|
||||
```
|
||||
"Send a mail from alice to bob welcoming him, then check bob's inbox to confirm delivery"
|
||||
```
|
||||
The agent will: call Mail.Send → call Mail.Read to verify
|
||||
|
||||
### 5. Platform Overview
|
||||
```
|
||||
"Show me all users, all posts, and all tags currently in use"
|
||||
```
|
||||
The agent will: call Users.List, Posts.List, and ListTags (potentially in parallel)
|
||||
|
||||
## How It Works
|
||||
|
||||
The key insight: **you don't need to write any agent-specific code**. The MCP gateway discovers services from the registry, extracts tool schemas from Go types, and generates descriptions from doc comments.
|
||||
|
||||
```go
|
||||
service := micro.NewService("platform",
|
||||
micro.Address(":9090"),
|
||||
mcp.WithMCP(":3001"), // This one line makes everything AI-accessible
|
||||
)
|
||||
|
||||
service.Handle(&Users{})
|
||||
service.Handle(&Posts{})
|
||||
service.Handle(&Comments{})
|
||||
service.Handle(&Mail{})
|
||||
```
|
||||
|
||||
Each handler method becomes an MCP tool. The `@example` tags in doc comments give agents sample inputs to learn from.
|
||||
|
||||
## Connecting to Claude Code
|
||||
|
||||
Add to your Claude Code MCP config:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"platform": {
|
||||
"command": "curl",
|
||||
"args": ["-s", "http://localhost:3001/mcp/tools"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or use stdio transport:
|
||||
|
||||
```bash
|
||||
micro mcp serve --registry mdns
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Agent (Claude, GPT, etc.)
|
||||
│
|
||||
▼
|
||||
MCP Gateway (:3001) ← Discovers services, generates tools
|
||||
│
|
||||
▼
|
||||
Go Micro RPC (:9090) ← Standard service mesh
|
||||
│
|
||||
├── UserService ← Signup, Login, Profile
|
||||
├── PostService ← CRUD + Tags
|
||||
├── CommentService ← Threaded comments
|
||||
└── MailService ← Internal messaging
|
||||
```
|
||||
|
||||
## Relation to micro/blog
|
||||
|
||||
This example is a simplified, self-contained version of [micro/blog](https://github.com/micro/blog). The real platform splits each service into its own binary with protobuf definitions. This example uses Go structs directly for simplicity, but the MCP integration works identically either way — the gateway discovers services from the registry regardless of how they're implemented.
|
||||
@@ -0,0 +1,774 @@
|
||||
// Platform example: AI agents interacting with a real microservices platform.
|
||||
//
|
||||
// This example mirrors the micro/blog platform (https://github.com/micro/blog)
|
||||
// — a microblogging platform built on Go Micro with Users, Posts, Comments,
|
||||
// and Mail services. It demonstrates how existing microservices become
|
||||
// AI-accessible through MCP with zero changes to business logic.
|
||||
//
|
||||
// The services run as a single binary for convenience. In production,
|
||||
// each would be a separate process discovered via the registry.
|
||||
//
|
||||
// Run:
|
||||
//
|
||||
// go run .
|
||||
//
|
||||
// MCP tools: http://localhost:3001/mcp/tools
|
||||
//
|
||||
// Agent scenarios:
|
||||
//
|
||||
// "Sign me up as alice with password secret123"
|
||||
// "Log in as alice and write a blog post about Go concurrency"
|
||||
// "List all posts and comment on the first one"
|
||||
// "Send a welcome email to alice"
|
||||
// "Tag the Go concurrency post with 'golang' and 'tutorial'"
|
||||
// "Show me alice's profile and all her posts"
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
"go-micro.dev/v6/gateway/mcp"
|
||||
"go-micro.dev/v6/server"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Users service — account registration, login, profiles
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id" description:"Unique user identifier"`
|
||||
Name string `json:"name" description:"Display name"`
|
||||
Status string `json:"status" description:"Bio or status message"`
|
||||
CreatedAt int64 `json:"created_at" description:"Unix timestamp of account creation"`
|
||||
}
|
||||
|
||||
type SignupRequest struct {
|
||||
Name string `json:"name" description:"Username (required, 3-20 characters)"`
|
||||
Password string `json:"password" description:"Password (required, minimum 6 characters)"`
|
||||
}
|
||||
type SignupResponse struct {
|
||||
User *User `json:"user" description:"The newly created user account"`
|
||||
Token string `json:"token" description:"Session token for authenticated requests"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Name string `json:"name" description:"Username"`
|
||||
Password string `json:"password" description:"Password"`
|
||||
}
|
||||
type LoginResponse struct {
|
||||
User *User `json:"user" description:"The authenticated user"`
|
||||
Token string `json:"token" description:"Session token for authenticated requests"`
|
||||
}
|
||||
|
||||
type GetProfileRequest struct {
|
||||
ID string `json:"id" description:"User ID to look up"`
|
||||
}
|
||||
type GetProfileResponse struct {
|
||||
User *User `json:"user" description:"The user profile"`
|
||||
}
|
||||
|
||||
type UpdateStatusRequest struct {
|
||||
ID string `json:"id" description:"User ID"`
|
||||
Status string `json:"status" description:"New bio or status message"`
|
||||
}
|
||||
type UpdateStatusResponse struct {
|
||||
User *User `json:"user" description:"Updated user profile"`
|
||||
}
|
||||
|
||||
type ListUsersRequest struct{}
|
||||
type ListUsersResponse struct {
|
||||
Users []*User `json:"users" description:"All registered users"`
|
||||
}
|
||||
|
||||
type Users struct {
|
||||
mu sync.RWMutex
|
||||
users map[string]*User
|
||||
passwords map[string]string // name -> password (plaintext for demo only)
|
||||
tokens map[string]string // token -> user ID
|
||||
nextID int
|
||||
}
|
||||
|
||||
func NewUsers() *Users {
|
||||
return &Users{
|
||||
users: make(map[string]*User),
|
||||
passwords: make(map[string]string),
|
||||
tokens: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// Signup creates a new user account and returns a session token.
|
||||
// The username must be unique. Use the returned token for authenticated operations.
|
||||
//
|
||||
// @example {"name": "alice", "password": "secret123"}
|
||||
func (s *Users) Signup(ctx context.Context, req *SignupRequest, rsp *SignupResponse) error {
|
||||
if req.Name == "" || len(req.Name) < 3 {
|
||||
return fmt.Errorf("name must be at least 3 characters")
|
||||
}
|
||||
if req.Password == "" || len(req.Password) < 6 {
|
||||
return fmt.Errorf("password must be at least 6 characters")
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Check uniqueness
|
||||
for _, u := range s.users {
|
||||
if strings.EqualFold(u.Name, req.Name) {
|
||||
return fmt.Errorf("username %q is already taken", req.Name)
|
||||
}
|
||||
}
|
||||
|
||||
s.nextID++
|
||||
user := &User{
|
||||
ID: fmt.Sprintf("user-%d", s.nextID),
|
||||
Name: req.Name,
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}
|
||||
s.users[user.ID] = user
|
||||
s.passwords[req.Name] = req.Password
|
||||
|
||||
token := generateToken()
|
||||
s.tokens[token] = user.ID
|
||||
|
||||
rsp.User = user
|
||||
rsp.Token = token
|
||||
return nil
|
||||
}
|
||||
|
||||
// Login authenticates a user and returns a session token.
|
||||
// Returns an error if the credentials are invalid.
|
||||
//
|
||||
// @example {"name": "alice", "password": "secret123"}
|
||||
func (s *Users) Login(ctx context.Context, req *LoginRequest, rsp *LoginResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
pass, ok := s.passwords[req.Name]
|
||||
if !ok || pass != req.Password {
|
||||
return fmt.Errorf("invalid username or password")
|
||||
}
|
||||
|
||||
// Find user by name
|
||||
for _, u := range s.users {
|
||||
if u.Name == req.Name {
|
||||
token := generateToken()
|
||||
s.tokens[token] = u.ID
|
||||
rsp.User = u
|
||||
rsp.Token = token
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
// GetProfile retrieves a user's public profile by ID.
|
||||
//
|
||||
// @example {"id": "user-1"}
|
||||
func (s *Users) GetProfile(ctx context.Context, req *GetProfileRequest, rsp *GetProfileResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
u, ok := s.users[req.ID]
|
||||
if !ok {
|
||||
return fmt.Errorf("user %s not found", req.ID)
|
||||
}
|
||||
rsp.User = u
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateStatus sets a user's bio or status message.
|
||||
//
|
||||
// @example {"id": "user-1", "status": "Writing about Go and microservices"}
|
||||
func (s *Users) UpdateStatus(ctx context.Context, req *UpdateStatusRequest, rsp *UpdateStatusResponse) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
u, ok := s.users[req.ID]
|
||||
if !ok {
|
||||
return fmt.Errorf("user %s not found", req.ID)
|
||||
}
|
||||
u.Status = req.Status
|
||||
rsp.User = u
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns all registered users on the platform.
|
||||
//
|
||||
// @example {}
|
||||
func (s *Users) List(ctx context.Context, req *ListUsersRequest, rsp *ListUsersResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
for _, u := range s.users {
|
||||
rsp.Users = append(rsp.Users, u)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Posts service — blog posts with markdown and tags
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type Post struct {
|
||||
ID string `json:"id" description:"Unique post identifier"`
|
||||
Title string `json:"title" description:"Post title"`
|
||||
Content string `json:"content" description:"Post body in markdown"`
|
||||
AuthorID string `json:"author_id" description:"ID of the post author"`
|
||||
AuthorName string `json:"author_name" description:"Display name of the author"`
|
||||
Tags []string `json:"tags,omitempty" description:"Post tags for categorization"`
|
||||
CreatedAt int64 `json:"created_at" description:"Unix timestamp of creation"`
|
||||
UpdatedAt int64 `json:"updated_at" description:"Unix timestamp of last update"`
|
||||
}
|
||||
|
||||
type CreatePostRequest struct {
|
||||
Title string `json:"title" description:"Post title (required)"`
|
||||
Content string `json:"content" description:"Post body in markdown (required)"`
|
||||
AuthorID string `json:"author_id" description:"Author's user ID (required)"`
|
||||
AuthorName string `json:"author_name" description:"Author's display name (required)"`
|
||||
}
|
||||
type CreatePostResponse struct {
|
||||
Post *Post `json:"post" description:"The newly created post"`
|
||||
}
|
||||
|
||||
type ReadPostRequest struct {
|
||||
ID string `json:"id" description:"Post ID to retrieve"`
|
||||
}
|
||||
type ReadPostResponse struct {
|
||||
Post *Post `json:"post" description:"The requested post"`
|
||||
}
|
||||
|
||||
type UpdatePostRequest struct {
|
||||
ID string `json:"id" description:"Post ID to update (required)"`
|
||||
Title string `json:"title" description:"New title"`
|
||||
Content string `json:"content" description:"New content in markdown"`
|
||||
}
|
||||
type UpdatePostResponse struct {
|
||||
Post *Post `json:"post" description:"The updated post"`
|
||||
}
|
||||
|
||||
type DeletePostRequest struct {
|
||||
ID string `json:"id" description:"Post ID to delete"`
|
||||
}
|
||||
type DeletePostResponse struct {
|
||||
Message string `json:"message" description:"Confirmation message"`
|
||||
}
|
||||
|
||||
type ListPostsRequest struct {
|
||||
AuthorID string `json:"author_id,omitempty" description:"Filter by author ID (optional)"`
|
||||
}
|
||||
type ListPostsResponse struct {
|
||||
Posts []*Post `json:"posts" description:"Posts in reverse chronological order"`
|
||||
Total int `json:"total" description:"Total number of matching posts"`
|
||||
}
|
||||
|
||||
type TagPostRequest struct {
|
||||
PostID string `json:"post_id" description:"Post to tag"`
|
||||
Tag string `json:"tag" description:"Tag to add (lowercase, no spaces)"`
|
||||
}
|
||||
type TagPostResponse struct {
|
||||
Post *Post `json:"post" description:"Post with updated tags"`
|
||||
}
|
||||
|
||||
type UntagPostRequest struct {
|
||||
PostID string `json:"post_id" description:"Post to untag"`
|
||||
Tag string `json:"tag" description:"Tag to remove"`
|
||||
}
|
||||
type UntagPostResponse struct {
|
||||
Post *Post `json:"post" description:"Post with updated tags"`
|
||||
}
|
||||
|
||||
type ListTagsRequest struct{}
|
||||
type ListTagsResponse struct {
|
||||
Tags []string `json:"tags" description:"All tags in use, sorted alphabetically"`
|
||||
}
|
||||
|
||||
type Posts struct {
|
||||
mu sync.RWMutex
|
||||
posts map[string]*Post
|
||||
nextID int
|
||||
}
|
||||
|
||||
func NewPosts() *Posts {
|
||||
return &Posts{posts: make(map[string]*Post)}
|
||||
}
|
||||
|
||||
// Create publishes a new blog post. Title, content, author_id, and author_name
|
||||
// are required. Content supports markdown formatting.
|
||||
//
|
||||
// @example {"title": "Getting Started with Go Micro", "content": "Go Micro makes it easy to build microservices...", "author_id": "user-1", "author_name": "alice"}
|
||||
func (s *Posts) Create(ctx context.Context, req *CreatePostRequest, rsp *CreatePostResponse) error {
|
||||
if req.Title == "" {
|
||||
return fmt.Errorf("title is required")
|
||||
}
|
||||
if req.Content == "" {
|
||||
return fmt.Errorf("content is required")
|
||||
}
|
||||
if req.AuthorID == "" {
|
||||
return fmt.Errorf("author_id is required")
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.nextID++
|
||||
now := time.Now().Unix()
|
||||
post := &Post{
|
||||
ID: fmt.Sprintf("post-%d", s.nextID),
|
||||
Title: req.Title,
|
||||
Content: req.Content,
|
||||
AuthorID: req.AuthorID,
|
||||
AuthorName: req.AuthorName,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
s.posts[post.ID] = post
|
||||
rsp.Post = post
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read retrieves a single blog post by ID.
|
||||
//
|
||||
// @example {"id": "post-1"}
|
||||
func (s *Posts) Read(ctx context.Context, req *ReadPostRequest, rsp *ReadPostResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
p, ok := s.posts[req.ID]
|
||||
if !ok {
|
||||
return fmt.Errorf("post %s not found", req.ID)
|
||||
}
|
||||
rsp.Post = p
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update modifies a blog post's title and/or content.
|
||||
// Only non-empty fields are updated.
|
||||
//
|
||||
// @example {"id": "post-1", "title": "Updated Title", "content": "New content here..."}
|
||||
func (s *Posts) Update(ctx context.Context, req *UpdatePostRequest, rsp *UpdatePostResponse) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
p, ok := s.posts[req.ID]
|
||||
if !ok {
|
||||
return fmt.Errorf("post %s not found", req.ID)
|
||||
}
|
||||
if req.Title != "" {
|
||||
p.Title = req.Title
|
||||
}
|
||||
if req.Content != "" {
|
||||
p.Content = req.Content
|
||||
}
|
||||
p.UpdatedAt = time.Now().Unix()
|
||||
rsp.Post = p
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes a blog post permanently.
|
||||
//
|
||||
// @example {"id": "post-1"}
|
||||
func (s *Posts) Delete(ctx context.Context, req *DeletePostRequest, rsp *DeletePostResponse) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, ok := s.posts[req.ID]; !ok {
|
||||
return fmt.Errorf("post %s not found", req.ID)
|
||||
}
|
||||
delete(s.posts, req.ID)
|
||||
rsp.Message = fmt.Sprintf("post %s deleted", req.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns blog posts in reverse chronological order.
|
||||
// Optionally filter by author_id to see a specific user's posts.
|
||||
//
|
||||
// @example {"author_id": "user-1"}
|
||||
func (s *Posts) List(ctx context.Context, req *ListPostsRequest, rsp *ListPostsResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
for _, p := range s.posts {
|
||||
if req.AuthorID != "" && p.AuthorID != req.AuthorID {
|
||||
continue
|
||||
}
|
||||
rsp.Posts = append(rsp.Posts, p)
|
||||
}
|
||||
sort.Slice(rsp.Posts, func(i, j int) bool {
|
||||
return rsp.Posts[i].CreatedAt > rsp.Posts[j].CreatedAt
|
||||
})
|
||||
rsp.Total = len(rsp.Posts)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TagPost adds a tag to a post. Tags are useful for categorization
|
||||
// and discovery. Duplicate tags are ignored.
|
||||
//
|
||||
// @example {"post_id": "post-1", "tag": "golang"}
|
||||
func (s *Posts) TagPost(ctx context.Context, req *TagPostRequest, rsp *TagPostResponse) error {
|
||||
if req.PostID == "" || req.Tag == "" {
|
||||
return fmt.Errorf("post_id and tag are required")
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
p, ok := s.posts[req.PostID]
|
||||
if !ok {
|
||||
return fmt.Errorf("post %s not found", req.PostID)
|
||||
}
|
||||
|
||||
tag := strings.ToLower(strings.TrimSpace(req.Tag))
|
||||
for _, t := range p.Tags {
|
||||
if t == tag {
|
||||
rsp.Post = p
|
||||
return nil
|
||||
}
|
||||
}
|
||||
p.Tags = append(p.Tags, tag)
|
||||
p.UpdatedAt = time.Now().Unix()
|
||||
rsp.Post = p
|
||||
return nil
|
||||
}
|
||||
|
||||
// UntagPost removes a tag from a post.
|
||||
//
|
||||
// @example {"post_id": "post-1", "tag": "golang"}
|
||||
func (s *Posts) UntagPost(ctx context.Context, req *UntagPostRequest, rsp *UntagPostResponse) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
p, ok := s.posts[req.PostID]
|
||||
if !ok {
|
||||
return fmt.Errorf("post %s not found", req.PostID)
|
||||
}
|
||||
|
||||
filtered := make([]string, 0, len(p.Tags))
|
||||
for _, t := range p.Tags {
|
||||
if t != req.Tag {
|
||||
filtered = append(filtered, t)
|
||||
}
|
||||
}
|
||||
p.Tags = filtered
|
||||
p.UpdatedAt = time.Now().Unix()
|
||||
rsp.Post = p
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListTags returns all tags currently in use across all posts.
|
||||
//
|
||||
// @example {}
|
||||
func (s *Posts) ListTags(ctx context.Context, req *ListTagsRequest, rsp *ListTagsResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
seen := make(map[string]bool)
|
||||
for _, p := range s.posts {
|
||||
for _, t := range p.Tags {
|
||||
seen[t] = true
|
||||
}
|
||||
}
|
||||
for t := range seen {
|
||||
rsp.Tags = append(rsp.Tags, t)
|
||||
}
|
||||
sort.Strings(rsp.Tags)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Comments service — threaded comments on posts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type Comment struct {
|
||||
ID string `json:"id" description:"Unique comment identifier"`
|
||||
PostID string `json:"post_id" description:"ID of the post this comment belongs to"`
|
||||
Content string `json:"content" description:"Comment text"`
|
||||
AuthorID string `json:"author_id" description:"ID of the comment author"`
|
||||
AuthorName string `json:"author_name" description:"Display name of the author"`
|
||||
CreatedAt int64 `json:"created_at" description:"Unix timestamp of creation"`
|
||||
}
|
||||
|
||||
type CreateCommentRequest struct {
|
||||
PostID string `json:"post_id" description:"Post to comment on (required)"`
|
||||
Content string `json:"content" description:"Comment text (required)"`
|
||||
AuthorID string `json:"author_id" description:"Author's user ID (required)"`
|
||||
AuthorName string `json:"author_name" description:"Author's display name (required)"`
|
||||
}
|
||||
type CreateCommentResponse struct {
|
||||
Comment *Comment `json:"comment" description:"The newly created comment"`
|
||||
}
|
||||
|
||||
type ListCommentsRequest struct {
|
||||
PostID string `json:"post_id,omitempty" description:"Filter by post ID (optional)"`
|
||||
AuthorID string `json:"author_id,omitempty" description:"Filter by author ID (optional)"`
|
||||
}
|
||||
type ListCommentsResponse struct {
|
||||
Comments []*Comment `json:"comments" description:"Matching comments"`
|
||||
}
|
||||
|
||||
type DeleteCommentRequest struct {
|
||||
ID string `json:"id" description:"Comment ID to delete"`
|
||||
}
|
||||
type DeleteCommentResponse struct {
|
||||
Message string `json:"message" description:"Confirmation message"`
|
||||
}
|
||||
|
||||
type Comments struct {
|
||||
mu sync.RWMutex
|
||||
comments []*Comment
|
||||
nextID int
|
||||
}
|
||||
|
||||
// Create adds a comment to a blog post. Post ID, content, author_id,
|
||||
// and author_name are all required.
|
||||
//
|
||||
// @example {"post_id": "post-1", "content": "Great article! Very helpful.", "author_id": "user-2", "author_name": "bob"}
|
||||
func (s *Comments) Create(ctx context.Context, req *CreateCommentRequest, rsp *CreateCommentResponse) error {
|
||||
if req.PostID == "" {
|
||||
return fmt.Errorf("post_id is required")
|
||||
}
|
||||
if req.Content == "" {
|
||||
return fmt.Errorf("content is required")
|
||||
}
|
||||
if req.AuthorID == "" {
|
||||
return fmt.Errorf("author_id is required")
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.nextID++
|
||||
comment := &Comment{
|
||||
ID: fmt.Sprintf("comment-%d", s.nextID),
|
||||
PostID: req.PostID,
|
||||
Content: req.Content,
|
||||
AuthorID: req.AuthorID,
|
||||
AuthorName: req.AuthorName,
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}
|
||||
s.comments = append(s.comments, comment)
|
||||
rsp.Comment = comment
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns comments, optionally filtered by post or author.
|
||||
// Use post_id to get all comments on a specific post.
|
||||
//
|
||||
// @example {"post_id": "post-1"}
|
||||
func (s *Comments) List(ctx context.Context, req *ListCommentsRequest, rsp *ListCommentsResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
for _, c := range s.comments {
|
||||
if req.PostID != "" && c.PostID != req.PostID {
|
||||
continue
|
||||
}
|
||||
if req.AuthorID != "" && c.AuthorID != req.AuthorID {
|
||||
continue
|
||||
}
|
||||
rsp.Comments = append(rsp.Comments, c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes a comment by ID.
|
||||
//
|
||||
// @example {"id": "comment-1"}
|
||||
func (s *Comments) Delete(ctx context.Context, req *DeleteCommentRequest, rsp *DeleteCommentResponse) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for i, c := range s.comments {
|
||||
if c.ID == req.ID {
|
||||
s.comments = append(s.comments[:i], s.comments[i+1:]...)
|
||||
rsp.Message = fmt.Sprintf("comment %s deleted", req.ID)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("comment %s not found", req.ID)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mail service — internal messaging between users
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type MailMessage struct {
|
||||
ID string `json:"id" description:"Unique message identifier"`
|
||||
From string `json:"from" description:"Sender username"`
|
||||
To string `json:"to" description:"Recipient username"`
|
||||
Subject string `json:"subject" description:"Message subject line"`
|
||||
Body string `json:"body" description:"Message body text"`
|
||||
Read bool `json:"read" description:"Whether the message has been read"`
|
||||
CreatedAt int64 `json:"created_at" description:"Unix timestamp of when the message was sent"`
|
||||
}
|
||||
|
||||
type SendMailRequest struct {
|
||||
From string `json:"from" description:"Sender username (required)"`
|
||||
To string `json:"to" description:"Recipient username (required)"`
|
||||
Subject string `json:"subject" description:"Message subject (required)"`
|
||||
Body string `json:"body" description:"Message body (required)"`
|
||||
}
|
||||
type SendMailResponse struct {
|
||||
Message *MailMessage `json:"message" description:"The sent message"`
|
||||
}
|
||||
|
||||
type ReadMailRequest struct {
|
||||
User string `json:"user" description:"Username to read inbox for"`
|
||||
}
|
||||
type ReadMailResponse struct {
|
||||
Messages []*MailMessage `json:"messages" description:"Inbox messages, newest first"`
|
||||
}
|
||||
|
||||
type Mail struct {
|
||||
mu sync.RWMutex
|
||||
messages []*MailMessage
|
||||
nextID int
|
||||
}
|
||||
|
||||
// Send delivers a message to another user on the platform.
|
||||
// Both sender and recipient are identified by username.
|
||||
//
|
||||
// @example {"from": "alice", "to": "bob", "subject": "Welcome!", "body": "Hey Bob, welcome to the platform!"}
|
||||
func (s *Mail) Send(ctx context.Context, req *SendMailRequest, rsp *SendMailResponse) error {
|
||||
if req.From == "" || req.To == "" {
|
||||
return fmt.Errorf("from and to are required")
|
||||
}
|
||||
if req.Subject == "" {
|
||||
return fmt.Errorf("subject is required")
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.nextID++
|
||||
msg := &MailMessage{
|
||||
ID: fmt.Sprintf("mail-%d", s.nextID),
|
||||
From: req.From,
|
||||
To: req.To,
|
||||
Subject: req.Subject,
|
||||
Body: req.Body,
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}
|
||||
s.messages = append(s.messages, msg)
|
||||
rsp.Message = msg
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read returns all messages in a user's inbox, newest first.
|
||||
//
|
||||
// @example {"user": "alice"}
|
||||
func (s *Mail) Read(ctx context.Context, req *ReadMailRequest, rsp *ReadMailResponse) error {
|
||||
if req.User == "" {
|
||||
return fmt.Errorf("user is required")
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for i := len(s.messages) - 1; i >= 0; i-- {
|
||||
if s.messages[i].To == req.User {
|
||||
s.messages[i].Read = true
|
||||
rsp.Messages = append(rsp.Messages, s.messages[i])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main — wire up all services with MCP gateway
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func main() {
|
||||
service := micro.NewService("platform",
|
||||
micro.Address(":9090"),
|
||||
mcp.WithMCP(":3001"),
|
||||
)
|
||||
service.Init()
|
||||
|
||||
users := NewUsers()
|
||||
posts := NewPosts()
|
||||
|
||||
// Seed some demo data so agents have something to work with
|
||||
seedData(users, posts)
|
||||
|
||||
service.Handle(users)
|
||||
service.Handle(posts)
|
||||
service.Handle(&Comments{})
|
||||
service.Handle(&Mail{},
|
||||
server.WithEndpointScopes("Mail.Send", "mail:write"),
|
||||
server.WithEndpointScopes("Mail.Read", "mail:read"),
|
||||
)
|
||||
|
||||
printBanner()
|
||||
|
||||
if err := service.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func seedData(users *Users, posts *Posts) {
|
||||
// Create demo users
|
||||
var aliceRsp SignupResponse
|
||||
users.Signup(context.Background(), &SignupRequest{
|
||||
Name: "alice", Password: "secret123",
|
||||
}, &aliceRsp)
|
||||
|
||||
var bobRsp SignupResponse
|
||||
users.Signup(context.Background(), &SignupRequest{
|
||||
Name: "bob", Password: "secret123",
|
||||
}, &bobRsp)
|
||||
|
||||
// Alice writes a welcome post
|
||||
var postRsp CreatePostResponse
|
||||
posts.Create(context.Background(), &CreatePostRequest{
|
||||
Title: "Welcome to the Platform",
|
||||
Content: "This is the first post on our new blogging platform. Built with Go Micro, every service is automatically accessible to AI agents through MCP.",
|
||||
AuthorID: aliceRsp.User.ID,
|
||||
AuthorName: "alice",
|
||||
}, &postRsp)
|
||||
|
||||
// Tag it
|
||||
posts.TagPost(context.Background(), &TagPostRequest{
|
||||
PostID: postRsp.Post.ID, Tag: "welcome",
|
||||
}, &TagPostResponse{})
|
||||
posts.TagPost(context.Background(), &TagPostRequest{
|
||||
PostID: postRsp.Post.ID, Tag: "go-micro",
|
||||
}, &TagPostResponse{})
|
||||
}
|
||||
|
||||
func printBanner() {
|
||||
fmt.Println()
|
||||
fmt.Println(" Platform Demo — AI-Native Microservices")
|
||||
fmt.Println()
|
||||
fmt.Println(" Services: Users, Posts, Comments, Mail")
|
||||
fmt.Println(" MCP Tools: http://localhost:3001/mcp/tools")
|
||||
fmt.Println(" RPC: localhost:9090")
|
||||
fmt.Println()
|
||||
fmt.Println(" Seeded: alice (user-1), bob (user-2)")
|
||||
fmt.Println(" 1 post with tags [welcome, go-micro]")
|
||||
fmt.Println()
|
||||
fmt.Println(" Try asking an agent:")
|
||||
fmt.Println()
|
||||
fmt.Println(` "Sign up a new user called carol"`)
|
||||
fmt.Println(` "Log in as alice and write a post about Go concurrency patterns"`)
|
||||
fmt.Println(` "List all posts and comment on the welcome post as bob"`)
|
||||
fmt.Println(` "Tag alice's post with 'tutorial' and 'golang'"`)
|
||||
fmt.Println(` "Send a mail from alice to bob welcoming him to the platform"`)
|
||||
fmt.Println(` "Show me bob's inbox"`)
|
||||
fmt.Println(` "List all users and show me all tags in use"`)
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func generateToken() string {
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
# Workflow Example: Cross-Service Orchestration
|
||||
|
||||
An e-commerce scenario with three services (Inventory, Orders, Notifications) that demonstrates how AI agents orchestrate multi-step workflows across services — no glue code, no workflow engine.
|
||||
|
||||
## The Workflow
|
||||
|
||||
When a user says _"Order a ThinkPad for alice and send her a confirmation"_, the agent figures out the steps:
|
||||
|
||||
```
|
||||
1. InventoryService.Search → Find the product
|
||||
2. InventoryService.CheckStock → Verify availability
|
||||
3. InventoryService.ReserveStock → Decrement inventory
|
||||
4. OrderService.PlaceOrder → Create the order
|
||||
5. NotificationService.Send → Email confirmation
|
||||
```
|
||||
|
||||
No code connects these steps — the agent reads the tool descriptions and chains the calls itself.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
go run .
|
||||
```
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Tools | Purpose |
|
||||
|---------|-------|---------|
|
||||
| InventoryService | Search, CheckStock, ReserveStock | Product catalog and stock management |
|
||||
| OrderService | PlaceOrder, GetOrder, ListOrders | Order creation and lookup |
|
||||
| NotificationService | Send, List | Email/SMS/Slack notifications |
|
||||
|
||||
## Example Prompts
|
||||
|
||||
Try these with Claude Code (`micro mcp serve`) or any MCP-compatible agent:
|
||||
|
||||
- "What laptops do you have in stock?"
|
||||
- "Order a ThinkPad for alice@example.com and send her a confirmation"
|
||||
- "Check if 'The Go Programming Language' is available" (it's out of stock!)
|
||||
- "Order 3 Go Gopher t-shirts for bob@example.com, reserve the stock, and notify him via Slack"
|
||||
- "Show me all orders and notifications for alice"
|
||||
|
||||
## Why This Matters
|
||||
|
||||
Traditional approach:
|
||||
```go
|
||||
// 50+ lines of glue code wiring services together
|
||||
func handleOrder(req OrderRequest) {
|
||||
product, err := inventoryClient.CheckStock(req.SKU)
|
||||
if err != nil { ... }
|
||||
if product.InStock < req.Quantity { ... }
|
||||
_, err = inventoryClient.ReserveStock(req.SKU, req.Quantity)
|
||||
if err != nil { ... }
|
||||
order, err := orderClient.PlaceOrder(...)
|
||||
if err != nil { ... }
|
||||
_, err = notificationClient.Send(...)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Agent approach:
|
||||
```
|
||||
User: "Order a ThinkPad for alice and confirm via email"
|
||||
Agent: [reads tool descriptions, chains 5 calls, handles the out-of-stock case]
|
||||
```
|
||||
|
||||
The agent handles the orchestration. You just write the individual services with good documentation.
|
||||
@@ -0,0 +1,393 @@
|
||||
// Workflow example: cross-service orchestration via AI agents.
|
||||
//
|
||||
// This example runs three services (Inventory, Orders, Notifications) and
|
||||
// demonstrates how an AI agent can orchestrate a multi-step workflow:
|
||||
//
|
||||
// 1. Check inventory for a product
|
||||
// 2. Place an order if in stock
|
||||
// 3. Send a confirmation notification
|
||||
//
|
||||
// The agent figures out the right sequence of calls on its own — no
|
||||
// workflow engine, no glue code, just natural language.
|
||||
//
|
||||
// Run:
|
||||
//
|
||||
// go run .
|
||||
//
|
||||
// MCP tools: http://localhost:3001/mcp/tools
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
"go-micro.dev/v6/gateway/mcp"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Inventory service
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type Product struct {
|
||||
SKU string `json:"sku" description:"Stock keeping unit identifier"`
|
||||
Name string `json:"name" description:"Product name"`
|
||||
Price float64 `json:"price" description:"Unit price in USD"`
|
||||
InStock int `json:"in_stock" description:"Number of units available"`
|
||||
Category string `json:"category" description:"Product category"`
|
||||
}
|
||||
|
||||
type CheckStockRequest struct {
|
||||
SKU string `json:"sku" description:"Product SKU to check"`
|
||||
}
|
||||
|
||||
type CheckStockResponse struct {
|
||||
Product *Product `json:"product" description:"Product details with current stock level"`
|
||||
}
|
||||
|
||||
type SearchProductsRequest struct {
|
||||
Query string `json:"query" description:"Search term to match against product name or category"`
|
||||
Category string `json:"category,omitempty" description:"Filter by category: electronics, clothing, books (optional)"`
|
||||
}
|
||||
|
||||
type SearchProductsResponse struct {
|
||||
Products []*Product `json:"products" description:"Products matching the search criteria"`
|
||||
}
|
||||
|
||||
type ReserveStockRequest struct {
|
||||
SKU string `json:"sku" description:"Product SKU to reserve"`
|
||||
Quantity int `json:"quantity" description:"Number of units to reserve"`
|
||||
}
|
||||
|
||||
type ReserveStockResponse struct {
|
||||
Reserved bool `json:"reserved" description:"True if stock was successfully reserved"`
|
||||
Remaining int `json:"remaining" description:"Units remaining after reservation"`
|
||||
Message string `json:"message" description:"Human-readable result message"`
|
||||
}
|
||||
|
||||
type InventoryService struct {
|
||||
mu sync.RWMutex
|
||||
products map[string]*Product
|
||||
}
|
||||
|
||||
// CheckStock returns the current stock level for a product.
|
||||
// Use this before placing an order to verify availability.
|
||||
//
|
||||
// @example {"sku": "LAPTOP-001"}
|
||||
func (s *InventoryService) CheckStock(ctx context.Context, req *CheckStockRequest, rsp *CheckStockResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
p, ok := s.products[req.SKU]
|
||||
if !ok {
|
||||
return fmt.Errorf("product %s not found", req.SKU)
|
||||
}
|
||||
rsp.Product = p
|
||||
return nil
|
||||
}
|
||||
|
||||
// Search finds products by name or category. Use this to help
|
||||
// users find what they're looking for before checking stock.
|
||||
//
|
||||
// @example {"query": "laptop"}
|
||||
func (s *InventoryService) Search(ctx context.Context, req *SearchProductsRequest, rsp *SearchProductsResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
q := strings.ToLower(req.Query)
|
||||
for _, p := range s.products {
|
||||
if req.Category != "" && !strings.EqualFold(p.Category, req.Category) {
|
||||
continue
|
||||
}
|
||||
if q == "" || strings.Contains(strings.ToLower(p.Name), q) || strings.Contains(strings.ToLower(p.Category), q) {
|
||||
rsp.Products = append(rsp.Products, p)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReserveStock decrements inventory for a product. Call this after
|
||||
// confirming stock is available. Returns an error if insufficient stock.
|
||||
//
|
||||
// @example {"sku": "LAPTOP-001", "quantity": 1}
|
||||
func (s *InventoryService) ReserveStock(ctx context.Context, req *ReserveStockRequest, rsp *ReserveStockResponse) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
p, ok := s.products[req.SKU]
|
||||
if !ok {
|
||||
return fmt.Errorf("product %s not found", req.SKU)
|
||||
}
|
||||
if p.InStock < req.Quantity {
|
||||
rsp.Reserved = false
|
||||
rsp.Remaining = p.InStock
|
||||
rsp.Message = fmt.Sprintf("insufficient stock: requested %d but only %d available", req.Quantity, p.InStock)
|
||||
return nil
|
||||
}
|
||||
p.InStock -= req.Quantity
|
||||
rsp.Reserved = true
|
||||
rsp.Remaining = p.InStock
|
||||
rsp.Message = fmt.Sprintf("reserved %d units of %s", req.Quantity, p.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Orders service
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type Order struct {
|
||||
ID string `json:"id" description:"Unique order identifier"`
|
||||
Customer string `json:"customer" description:"Customer name or email"`
|
||||
SKU string `json:"sku" description:"Product SKU ordered"`
|
||||
Quantity int `json:"quantity" description:"Number of units"`
|
||||
Total float64 `json:"total" description:"Total order amount in USD"`
|
||||
Status string `json:"status" description:"Order status: pending, confirmed, shipped, delivered"`
|
||||
CreatedAt time.Time `json:"created_at" description:"When the order was placed"`
|
||||
}
|
||||
|
||||
type PlaceOrderRequest struct {
|
||||
Customer string `json:"customer" description:"Customer name or email (required)"`
|
||||
SKU string `json:"sku" description:"Product SKU to order (required)"`
|
||||
Quantity int `json:"quantity" description:"Number of units (required, must be positive)"`
|
||||
}
|
||||
|
||||
type PlaceOrderResponse struct {
|
||||
Order *Order `json:"order" description:"The newly created order"`
|
||||
}
|
||||
|
||||
type GetOrderRequest struct {
|
||||
ID string `json:"id" description:"Order ID to look up"`
|
||||
}
|
||||
|
||||
type GetOrderResponse struct {
|
||||
Order *Order `json:"order" description:"The requested order"`
|
||||
}
|
||||
|
||||
type ListOrdersRequest struct {
|
||||
Customer string `json:"customer,omitempty" description:"Filter by customer (optional)"`
|
||||
Status string `json:"status,omitempty" description:"Filter by status (optional)"`
|
||||
}
|
||||
|
||||
type ListOrdersResponse struct {
|
||||
Orders []*Order `json:"orders" description:"Matching orders"`
|
||||
}
|
||||
|
||||
type OrderService struct {
|
||||
mu sync.RWMutex
|
||||
orders map[string]*Order
|
||||
nextID int
|
||||
// In a real app this would be a client to the inventory service
|
||||
inventory *InventoryService
|
||||
}
|
||||
|
||||
// PlaceOrder creates a new order. Stock must be reserved first via
|
||||
// InventoryService.ReserveStock — this service does not check inventory.
|
||||
//
|
||||
// @example {"customer": "alice@example.com", "sku": "LAPTOP-001", "quantity": 1}
|
||||
func (s *OrderService) PlaceOrder(ctx context.Context, req *PlaceOrderRequest, rsp *PlaceOrderResponse) error {
|
||||
if req.Customer == "" {
|
||||
return fmt.Errorf("customer is required")
|
||||
}
|
||||
if req.SKU == "" {
|
||||
return fmt.Errorf("sku is required")
|
||||
}
|
||||
if req.Quantity <= 0 {
|
||||
return fmt.Errorf("quantity must be positive")
|
||||
}
|
||||
|
||||
// Look up price
|
||||
s.inventory.mu.RLock()
|
||||
p, ok := s.inventory.products[req.SKU]
|
||||
s.inventory.mu.RUnlock()
|
||||
if !ok {
|
||||
return fmt.Errorf("product %s not found", req.SKU)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.nextID++
|
||||
order := &Order{
|
||||
ID: fmt.Sprintf("ORD-%04d", s.nextID),
|
||||
Customer: req.Customer,
|
||||
SKU: req.SKU,
|
||||
Quantity: req.Quantity,
|
||||
Total: p.Price * float64(req.Quantity),
|
||||
Status: "confirmed",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
s.orders[order.ID] = order
|
||||
rsp.Order = order
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOrder retrieves an order by ID.
|
||||
//
|
||||
// @example {"id": "ORD-0001"}
|
||||
func (s *OrderService) GetOrder(ctx context.Context, req *GetOrderRequest, rsp *GetOrderResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
o, ok := s.orders[req.ID]
|
||||
if !ok {
|
||||
return fmt.Errorf("order %s not found", req.ID)
|
||||
}
|
||||
rsp.Order = o
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListOrders returns orders, optionally filtered by customer or status.
|
||||
//
|
||||
// @example {"customer": "alice@example.com"}
|
||||
func (s *OrderService) ListOrders(ctx context.Context, req *ListOrdersRequest, rsp *ListOrdersResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, o := range s.orders {
|
||||
if req.Customer != "" && o.Customer != req.Customer {
|
||||
continue
|
||||
}
|
||||
if req.Status != "" && o.Status != req.Status {
|
||||
continue
|
||||
}
|
||||
rsp.Orders = append(rsp.Orders, o)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Notifications service
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type Notification struct {
|
||||
ID string `json:"id" description:"Notification identifier"`
|
||||
Recipient string `json:"recipient" description:"Who received the notification"`
|
||||
Subject string `json:"subject" description:"Notification subject line"`
|
||||
Body string `json:"body" description:"Notification body text"`
|
||||
Channel string `json:"channel" description:"Delivery channel: email, sms, or slack"`
|
||||
SentAt time.Time `json:"sent_at" description:"When the notification was sent"`
|
||||
}
|
||||
|
||||
type SendNotificationRequest struct {
|
||||
Recipient string `json:"recipient" description:"Email address, phone number, or Slack handle"`
|
||||
Subject string `json:"subject" description:"Subject line (required)"`
|
||||
Body string `json:"body" description:"Message body (required)"`
|
||||
Channel string `json:"channel,omitempty" description:"Channel: email (default), sms, or slack"`
|
||||
}
|
||||
|
||||
type SendNotificationResponse struct {
|
||||
Notification *Notification `json:"notification" description:"The sent notification with delivery details"`
|
||||
}
|
||||
|
||||
type ListNotificationsRequest struct {
|
||||
Recipient string `json:"recipient,omitempty" description:"Filter by recipient (optional)"`
|
||||
}
|
||||
|
||||
type ListNotificationsResponse struct {
|
||||
Notifications []*Notification `json:"notifications" description:"Sent notifications"`
|
||||
}
|
||||
|
||||
type NotificationService struct {
|
||||
mu sync.RWMutex
|
||||
notifications []*Notification
|
||||
nextID int
|
||||
}
|
||||
|
||||
// Send delivers a notification to a recipient via the specified channel.
|
||||
// Use this to confirm orders, alert users, or send updates.
|
||||
// Defaults to email if no channel is specified.
|
||||
//
|
||||
// @example {"recipient": "alice@example.com", "subject": "Order Confirmed", "body": "Your order ORD-0001 has been confirmed.", "channel": "email"}
|
||||
func (s *NotificationService) Send(ctx context.Context, req *SendNotificationRequest, rsp *SendNotificationResponse) error {
|
||||
if req.Recipient == "" {
|
||||
return fmt.Errorf("recipient is required")
|
||||
}
|
||||
if req.Subject == "" {
|
||||
return fmt.Errorf("subject is required")
|
||||
}
|
||||
if req.Body == "" {
|
||||
return fmt.Errorf("body is required")
|
||||
}
|
||||
channel := req.Channel
|
||||
if channel == "" {
|
||||
channel = "email"
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.nextID++
|
||||
n := &Notification{
|
||||
ID: fmt.Sprintf("notif-%d", s.nextID),
|
||||
Recipient: req.Recipient,
|
||||
Subject: req.Subject,
|
||||
Body: req.Body,
|
||||
Channel: channel,
|
||||
SentAt: time.Now(),
|
||||
}
|
||||
s.notifications = append(s.notifications, n)
|
||||
rsp.Notification = n
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns sent notifications, optionally filtered by recipient.
|
||||
//
|
||||
// @example {"recipient": "alice@example.com"}
|
||||
func (s *NotificationService) List(ctx context.Context, req *ListNotificationsRequest, rsp *ListNotificationsResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, n := range s.notifications {
|
||||
if req.Recipient != "" && n.Recipient != req.Recipient {
|
||||
continue
|
||||
}
|
||||
rsp.Notifications = append(rsp.Notifications, n)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func main() {
|
||||
service := micro.NewService("shop",
|
||||
micro.Address(":9090"),
|
||||
mcp.WithMCP(":3001"),
|
||||
)
|
||||
service.Init()
|
||||
|
||||
inventory := &InventoryService{products: map[string]*Product{
|
||||
"LAPTOP-001": {SKU: "LAPTOP-001", Name: "ThinkPad X1 Carbon", Price: 1299.99, InStock: 15, Category: "electronics"},
|
||||
"LAPTOP-002": {SKU: "LAPTOP-002", Name: "MacBook Air M3", Price: 1099.00, InStock: 8, Category: "electronics"},
|
||||
"PHONE-001": {SKU: "PHONE-001", Name: "Pixel 8 Pro", Price: 899.00, InStock: 23, Category: "electronics"},
|
||||
"BOOK-001": {SKU: "BOOK-001", Name: "Designing Data-Intensive Applications", Price: 45.99, InStock: 50, Category: "books"},
|
||||
"BOOK-002": {SKU: "BOOK-002", Name: "The Go Programming Language", Price: 39.99, InStock: 0, Category: "books"},
|
||||
"SHIRT-001": {SKU: "SHIRT-001", Name: "Go Gopher T-Shirt", Price: 24.99, InStock: 100, Category: "clothing"},
|
||||
}}
|
||||
|
||||
orders := &OrderService{
|
||||
orders: make(map[string]*Order),
|
||||
inventory: inventory,
|
||||
}
|
||||
|
||||
notifications := &NotificationService{}
|
||||
|
||||
service.Handle(inventory)
|
||||
service.Handle(orders)
|
||||
service.Handle(notifications)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println(" Shop Workflow Demo")
|
||||
fmt.Println()
|
||||
fmt.Println(" MCP Tools: http://localhost:3001/mcp/tools")
|
||||
fmt.Println()
|
||||
fmt.Println(" Try asking an agent:")
|
||||
fmt.Println()
|
||||
fmt.Println(" \"What laptops do you have in stock?\"")
|
||||
fmt.Println(" \"Order a ThinkPad for alice@example.com and send her a confirmation\"")
|
||||
fmt.Println(" \"Check if 'The Go Programming Language' is available\"")
|
||||
fmt.Println(" \"Show me all orders for alice@example.com\"")
|
||||
fmt.Println(" \"Order 3 Go Gopher t-shirts for bob@example.com, reserve the stock, and notify him\"")
|
||||
fmt.Println()
|
||||
|
||||
if err := service.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Multi-service example: run multiple services in a single binary.
|
||||
//
|
||||
// Each service gets its own server, client, store, and cache while
|
||||
// sharing the registry, broker, and transport — so they can
|
||||
// discover and call each other within the same process.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
)
|
||||
|
||||
// -- Users service --
|
||||
|
||||
type UserRequest struct {
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
type UserResponse struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
type Users struct{}
|
||||
|
||||
func (u *Users) Lookup(ctx context.Context, req *UserRequest, rsp *UserResponse) error {
|
||||
log.Printf("[users] Lookup id=%s", req.Id)
|
||||
rsp.Name = "Alice"
|
||||
rsp.Email = "alice@example.com"
|
||||
return nil
|
||||
}
|
||||
|
||||
// -- Orders service --
|
||||
|
||||
type OrderRequest struct {
|
||||
UserId string `json:"user_id"`
|
||||
}
|
||||
|
||||
type OrderResponse struct {
|
||||
OrderId string `json:"order_id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type Orders struct{}
|
||||
|
||||
func (o *Orders) Create(ctx context.Context, req *OrderRequest, rsp *OrderResponse) error {
|
||||
log.Printf("[orders] Create for user=%s", req.UserId)
|
||||
rsp.OrderId = "ORD-001"
|
||||
rsp.Status = "created"
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Create two services — each gets isolated server, client,
|
||||
// store, and cache instances automatically.
|
||||
users := micro.NewService("users", micro.Address(":9001"))
|
||||
orders := micro.NewService("orders", micro.Address(":9002"))
|
||||
|
||||
// Register handlers
|
||||
if err := users.Handle(new(Users)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := orders.Handle(new(Orders)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Run both services together. The group handles signals
|
||||
// and stops all services when one exits.
|
||||
g := micro.NewGroup(users, orders)
|
||||
|
||||
fmt.Println("Starting users (:9001) and orders (:9002) in a single binary")
|
||||
if err := g.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
# Zero-to-hero support desk
|
||||
|
||||
A maintained 0-to-hero reference for the Go Micro lifecycle: scaffold a few
|
||||
typed services, run them in one process, let an agent chat with those services
|
||||
as tools, then inspect the durable flow that triggered the work. It is one
|
||||
runnable file and one CI smoke test, so the reference path stays honest as the
|
||||
framework evolves.
|
||||
|
||||
## The path
|
||||
|
||||
1. **Scaffold services** — `customers`, `tickets`, and `notify` are ordinary
|
||||
typed Go Micro services. Their request/response structs and method comments
|
||||
become the tool contract the agent sees.
|
||||
2. **Run the harness** — the example starts an in-memory registry, broker,
|
||||
client, store, services, agent, and flow in one process; no external
|
||||
dependencies or API key are required for the default run.
|
||||
3. **Chat through an agent** — the `support` agent receives the ticket event as
|
||||
a prompt and calls service tools to look up the customer, triage the ticket,
|
||||
and draft a reply.
|
||||
4. **Inspect the workflow** — the `intake` flow records the event-driven run and
|
||||
prints the agent result, showing the service → agent → workflow lifecycle as
|
||||
one runtime.
|
||||
|
||||
## The scenario
|
||||
|
||||
A customer files a ticket. A `ticket.created` event triggers the support
|
||||
agent, which:
|
||||
|
||||
1. looks the customer up (`customers` service),
|
||||
2. sets the ticket's priority (`tickets` service),
|
||||
3. drafts a reply and emails it (`notify` service) — **but only after passing
|
||||
the approval gate.**
|
||||
|
||||
```
|
||||
> event: events.ticket.created {"id":"ticket-1","customer":"alice@acme.com",...}
|
||||
|
||||
[customers] looked up Alice (pro plan)
|
||||
[tickets] ticket-1 → priority=high status=in_progress
|
||||
▣ approval gate notify_NotifyService_Send(alice@acme.com) — approved
|
||||
[notify] 📨 to=alice@acme.com: "Hi Alice — thanks for reaching out..."
|
||||
|
||||
✓ ticket triaged and the customer was replied to — triggered by an event
|
||||
```
|
||||
|
||||
## The pieces
|
||||
|
||||
- **Services** (`customers`, `tickets`, `notify`) — plain Go Micro services. The
|
||||
agent discovers their endpoints as tools automatically.
|
||||
- **Agent** (`support`) — `micro.NewAgent` with those three services. It reasons
|
||||
over the ticket and calls the tools.
|
||||
- **Flow** (`intake`) — triggers on `events.ticket.created` and hands the event to
|
||||
the agent: *the event is the prompt*. No human types anything.
|
||||
- **Guardrail** (`ApproveTool`) — the agent can read and triage freely, but
|
||||
emailing a customer (`notify.Send`) passes through the gate first. Return
|
||||
`false` to hold it for a person or a policy; the example approves and logs.
|
||||
|
||||
## Expected inspect transcript
|
||||
|
||||
The provider-free run prints the same visible checkpoints a new developer should
|
||||
compare against after chat and flow execution. The transcript includes service
|
||||
tool calls, the approval gate, and the inspect/run-history commands that prove
|
||||
the workflow run was recorded.
|
||||
|
||||
```text
|
||||
> event: events.ticket.created {"customer":"alice@acme.com","id":"ticket-1","subject":"Can't log in"}
|
||||
|
||||
[customers] looked up Alice (pro plan)
|
||||
[tickets] ticket-1 → priority=high status=in_progress
|
||||
▣ approval gate notify_NotifyService_Send(alice@acme.com) — approved
|
||||
[notify] 📨 to=alice@acme.com: "Hi Alice — thanks for reaching out. We've bumped this to high priority and are on it."
|
||||
|
||||
support agent: Triaged ticket-1 for Alice and sent a reply.
|
||||
|
||||
inspect transcript:
|
||||
micro inspect flow intake
|
||||
flow: intake runs=1 latest.reply="Triaged ticket-1 for Alice and sent a reply."
|
||||
micro agent history support
|
||||
agent: support runs=1 latest.status=completed
|
||||
|
||||
✓ ticket triaged and the customer was replied to — triggered by an event
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
go run main.go # mock model — deterministic, no API key
|
||||
```
|
||||
|
||||
The maintained check is the same deterministic path:
|
||||
|
||||
```bash
|
||||
go test ./examples/support
|
||||
```
|
||||
|
||||
Against a live model, the agent reasons about the ticket itself instead of
|
||||
following the script:
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY=sk-ant-... # or OPENAI_API_KEY, GEMINI_API_KEY, ...
|
||||
go run main.go -provider anthropic
|
||||
```
|
||||
|
||||
## What to change next
|
||||
|
||||
- Make the gate real: return `false` from `ApproveTool` for billing actions, or
|
||||
route the decision to a human.
|
||||
- Expose the agent over A2A so another team's agent can file tickets — add
|
||||
`agent.WithA2A(":4000")`.
|
||||
- Add a `kb` (knowledge base) service and watch the agent search it before
|
||||
replying.
|
||||
@@ -0,0 +1,329 @@
|
||||
// Support desk — a real-world agent built from services, a flow, and a
|
||||
// guardrail. It's the "zero to hero" shape: a few services, an agent that
|
||||
// manages them, an event that triggers the agent, and a human-in-the-loop
|
||||
// gate on the one action that touches a customer.
|
||||
//
|
||||
// The scenario: a customer files a ticket. A ticket.created event triggers
|
||||
// the support agent, which looks the customer up, sets the ticket's
|
||||
// priority, and drafts a reply — but it can't actually email the customer
|
||||
// without passing the approval gate.
|
||||
//
|
||||
// Everything is real — services, registry, RPC, broker, the agent loop,
|
||||
// the store. Only the LLM is mocked, so it runs with no API key. Pass
|
||||
// -provider anthropic (with a key) to run it against a live model; then the
|
||||
// agent reasons about the ticket itself instead of following the script.
|
||||
//
|
||||
// Run:
|
||||
//
|
||||
// go run ./examples/support
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6/agent"
|
||||
"go-micro.dev/v6/ai"
|
||||
"go-micro.dev/v6/broker"
|
||||
"go-micro.dev/v6/client"
|
||||
"go-micro.dev/v6/flow"
|
||||
"go-micro.dev/v6/registry"
|
||||
"go-micro.dev/v6/selector"
|
||||
"go-micro.dev/v6/service"
|
||||
"go-micro.dev/v6/store"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// services — the support desk's capabilities
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type Customer struct {
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Plan string `json:"plan"`
|
||||
}
|
||||
|
||||
type LookupRequest struct {
|
||||
Email string `json:"email" description:"Customer email to look up (required)"`
|
||||
}
|
||||
|
||||
// CustomerService is a tiny seeded customer directory.
|
||||
type CustomerService struct{}
|
||||
|
||||
// Lookup returns the customer with the given email.
|
||||
// @example {"email": "alice@acme.com"}
|
||||
func (s *CustomerService) Lookup(_ context.Context, req *LookupRequest, rsp *Customer) error {
|
||||
known := map[string]Customer{
|
||||
"alice@acme.com": {Email: "alice@acme.com", Name: "Alice", Plan: "pro"},
|
||||
}
|
||||
c, ok := known[req.Email]
|
||||
if !ok {
|
||||
return fmt.Errorf("customer %s not found", req.Email)
|
||||
}
|
||||
*rsp = c
|
||||
fmt.Printf(" \033[32m[customers]\033[0m looked up %s (%s plan)\n", c.Name, c.Plan)
|
||||
return nil
|
||||
}
|
||||
|
||||
type Ticket struct {
|
||||
ID string `json:"id"`
|
||||
Customer string `json:"customer"`
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
Priority string `json:"priority,omitempty"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type UpdateRequest struct {
|
||||
ID string `json:"id" description:"Ticket id (required)"`
|
||||
Priority string `json:"priority" description:"Priority: low, normal, high"`
|
||||
Status string `json:"status" description:"Status: open, in_progress, resolved"`
|
||||
}
|
||||
|
||||
// TicketService stores support tickets in memory.
|
||||
type TicketService struct {
|
||||
mu sync.Mutex
|
||||
tickets map[string]*Ticket
|
||||
}
|
||||
|
||||
func (s *TicketService) seed(t *Ticket) {
|
||||
s.mu.Lock()
|
||||
if s.tickets == nil {
|
||||
s.tickets = map[string]*Ticket{}
|
||||
}
|
||||
s.tickets[t.ID] = t
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Update changes a ticket's priority and/or status.
|
||||
// @example {"id": "ticket-1", "priority": "high", "status": "in_progress"}
|
||||
func (s *TicketService) Update(_ context.Context, req *UpdateRequest, rsp *Ticket) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
t, ok := s.tickets[req.ID]
|
||||
if !ok {
|
||||
return fmt.Errorf("ticket %s not found", req.ID)
|
||||
}
|
||||
if req.Priority != "" {
|
||||
t.Priority = req.Priority
|
||||
}
|
||||
if req.Status != "" {
|
||||
t.Status = req.Status
|
||||
}
|
||||
*rsp = *t
|
||||
fmt.Printf(" \033[32m[tickets]\033[0m %s → priority=%s status=%s\n", t.ID, t.Priority, t.Status)
|
||||
return nil
|
||||
}
|
||||
|
||||
type SendRequest struct {
|
||||
To string `json:"to" description:"Recipient email (required)"`
|
||||
Message string `json:"message" description:"Reply body (required)"`
|
||||
}
|
||||
type SendResponse struct {
|
||||
Sent bool `json:"sent"`
|
||||
}
|
||||
|
||||
// NotifyService emails the customer. This is the action we gate.
|
||||
type NotifyService struct{ sent int }
|
||||
|
||||
// Send emails a reply to a customer.
|
||||
// @example {"to": "alice@acme.com", "message": "We're on it."}
|
||||
func (s *NotifyService) Send(_ context.Context, req *SendRequest, rsp *SendResponse) error {
|
||||
s.sent++
|
||||
fmt.Printf(" \033[35m[notify]\033[0m 📨 to=%s: %q\n", req.To, req.Message)
|
||||
rsp.Sent = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// mock LLM — the only fake. It scripts the triage from the offered tools.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type mockModel struct{ opts ai.Options }
|
||||
|
||||
func newMock(opts ...ai.Option) ai.Model {
|
||||
m := &mockModel{}
|
||||
_ = m.Init(opts...)
|
||||
return m
|
||||
}
|
||||
func (m *mockModel) Init(opts ...ai.Option) error {
|
||||
for _, o := range opts {
|
||||
o(&m.opts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *mockModel) Options() ai.Options { return m.opts }
|
||||
func (m *mockModel) String() string { return "mock" }
|
||||
func (m *mockModel) Stream(context.Context, *ai.Request, ...ai.GenerateOption) (ai.Stream, error) {
|
||||
return nil, fmt.Errorf("stream not supported by mock")
|
||||
}
|
||||
|
||||
func (m *mockModel) call(ctx context.Context, tools []ai.Tool, sub string, input map[string]any) {
|
||||
for _, t := range tools {
|
||||
if strings.Contains(t.Name, sub) && m.opts.ToolHandler != nil {
|
||||
m.opts.ToolHandler(ctx, ai.ToolCall{ID: sub, Name: t.Name, Input: input})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockModel) Generate(ctx context.Context, req *ai.Request, _ ...ai.GenerateOption) (*ai.Response, error) {
|
||||
// A real model would read the ticket from the prompt and decide. The
|
||||
// mock follows a fixed, sensible triage so the demo is deterministic.
|
||||
m.call(ctx, req.Tools, "Lookup", map[string]any{"email": "alice@acme.com"})
|
||||
m.call(ctx, req.Tools, "Update", map[string]any{"id": "ticket-1", "priority": "high", "status": "in_progress"})
|
||||
m.call(ctx, req.Tools, "Send", map[string]any{
|
||||
"to": "alice@acme.com",
|
||||
"message": "Hi Alice — thanks for reaching out. We've bumped this to high priority and are on it.",
|
||||
})
|
||||
return &ai.Response{Answer: "Triaged ticket-1 for Alice and sent a reply."}, nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// wiring
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func providerKey(provider string) string {
|
||||
if v := os.Getenv("MICRO_AI_API_KEY"); v != "" {
|
||||
return v
|
||||
}
|
||||
return os.Getenv(map[string]string{
|
||||
"anthropic": "ANTHROPIC_API_KEY", "openai": "OPENAI_API_KEY",
|
||||
"gemini": "GEMINI_API_KEY", "groq": "GROQ_API_KEY", "mistral": "MISTRAL_API_KEY",
|
||||
"together": "TOGETHER_API_KEY", "atlascloud": "ATLASCLOUD_API_KEY",
|
||||
}[provider])
|
||||
}
|
||||
|
||||
func waitFor(reg registry.Registry, names ...string) {
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for _, name := range names {
|
||||
for time.Now().Before(deadline) {
|
||||
if svcs, err := reg.GetService(name); err == nil && len(svcs) > 0 && len(svcs[0].Nodes) > 0 {
|
||||
break
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runSupport(provider string) error {
|
||||
apiKey := ""
|
||||
if provider == "mock" {
|
||||
ai.Register("mock", newMock)
|
||||
} else if apiKey = providerKey(provider); apiKey == "" {
|
||||
return fmt.Errorf("no API key for provider %q — set MICRO_AI_API_KEY or the provider's key env", provider)
|
||||
}
|
||||
|
||||
fmt.Printf("\n\033[1mSupport desk (provider: %s)\033[0m\n\n", provider)
|
||||
|
||||
// Shared in-memory infrastructure so the demo runs in one process.
|
||||
reg := registry.NewMemoryRegistry()
|
||||
br := broker.NewMemoryBroker()
|
||||
if err := br.Connect(); err != nil {
|
||||
return fmt.Errorf("broker connect: %w", err)
|
||||
}
|
||||
cl := client.NewClient(client.Registry(reg), client.Selector(selector.NewSelector(selector.Registry(reg))))
|
||||
|
||||
// Services.
|
||||
tickets := new(TicketService)
|
||||
notify := new(NotifyService)
|
||||
var services []service.Service
|
||||
for name, h := range map[string]any{"customers": new(CustomerService), "tickets": tickets, "notify": notify} {
|
||||
svc := service.New(service.Name(name), service.Address("127.0.0.1:0"), service.Registry(reg), service.Client(cl), service.HandleSignal(false))
|
||||
_ = svc.Handle(h)
|
||||
services = append(services, svc)
|
||||
go svc.Run()
|
||||
}
|
||||
defer func() {
|
||||
for _, svc := range services {
|
||||
_ = svc.Server().Stop()
|
||||
}
|
||||
}()
|
||||
|
||||
// The support agent manages the three services. The approval gate is
|
||||
// the human-in-the-loop: it can read and triage freely, but emailing a
|
||||
// customer (notify.Send) passes through here first. Return false to hold
|
||||
// it for a person or a policy; here we approve and log.
|
||||
support := agent.New(
|
||||
agent.Name("support"),
|
||||
agent.Address("127.0.0.1:0"),
|
||||
agent.Services("customers", "tickets", "notify"),
|
||||
agent.Prompt("You are a support agent. For each ticket, look up the customer, set an "+
|
||||
"appropriate priority, and reply to them. Escalate billing issues."),
|
||||
agent.Provider(provider), agent.APIKey(apiKey),
|
||||
agent.ApproveTool(func(tool string, input map[string]any) (bool, string) {
|
||||
if strings.Contains(tool, "Send") {
|
||||
fmt.Printf(" \033[33m▣ approval gate\033[0m %s(%v) — approved\n", tool, input["to"])
|
||||
}
|
||||
return true, ""
|
||||
}),
|
||||
agent.WithRegistry(reg), agent.WithClient(cl), agent.WithStore(store.NewMemoryStore()),
|
||||
)
|
||||
go support.Run()
|
||||
defer support.Stop()
|
||||
|
||||
waitFor(reg, "customers", "tickets", "notify", "support")
|
||||
|
||||
// A new ticket arrives, and a flow turns the event into work for the
|
||||
// agent: the event is the prompt.
|
||||
intake := flow.New("intake",
|
||||
flow.Trigger("events.ticket.created"),
|
||||
flow.Agent("support"),
|
||||
flow.Prompt("A new support ticket arrived: {{.Data}}. Handle it."),
|
||||
)
|
||||
if err := intake.Register(reg, br, cl); err != nil {
|
||||
return fmt.Errorf("flow register: %w", err)
|
||||
}
|
||||
defer intake.Stop()
|
||||
|
||||
// The customer files a ticket (it exists before the event fires).
|
||||
tickets.seed(&Ticket{ID: "ticket-1", Customer: "alice@acme.com", Subject: "Can't log in", Body: "I'm locked out.", Status: "open"})
|
||||
body, _ := json.Marshal(map[string]string{"id": "ticket-1", "customer": "alice@acme.com", "subject": "Can't log in"})
|
||||
|
||||
fmt.Println("\033[1m> event:\033[0m events.ticket.created", string(body))
|
||||
fmt.Println()
|
||||
if err := br.Publish("events.ticket.created", &broker.Message{Body: body}); err != nil {
|
||||
return fmt.Errorf("publish: %w", err)
|
||||
}
|
||||
|
||||
// Wait for the agent to act.
|
||||
deadline := time.Now().Add(20 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if notify.sent >= 1 {
|
||||
break
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
|
||||
if rs := intake.Results(); len(rs) > 0 {
|
||||
latest := rs[len(rs)-1]
|
||||
fmt.Printf("\n\033[1msupport agent:\033[0m %s\n", latest.Reply)
|
||||
fmt.Println("\n\033[1minspect transcript:\033[0m")
|
||||
fmt.Println(" micro inspect flow intake")
|
||||
fmt.Printf(" flow: intake runs=%d latest.reply=%q\n", len(rs), latest.Reply)
|
||||
fmt.Println(" micro agent history support")
|
||||
fmt.Printf(" agent: support runs=%d latest.status=completed\n", len(rs))
|
||||
}
|
||||
if notify.sent >= 1 {
|
||||
fmt.Println("\n\033[32m✓ ticket triaged and the customer was replied to — triggered by an event\033[0m")
|
||||
return nil
|
||||
}
|
||||
fmt.Println("\n\033[31m✗ the agent did not complete the triage\033[0m")
|
||||
return fmt.Errorf("support agent did not complete triage")
|
||||
}
|
||||
|
||||
func main() {
|
||||
provider := flag.String("provider", "mock", "LLM provider: mock (default), anthropic, openai, ...")
|
||||
flag.Parse()
|
||||
|
||||
if err := runSupport(*provider); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRunSupportMockSmoke(t *testing.T) {
|
||||
if err := runSupport("mock"); err != nil {
|
||||
t.Fatalf("support example failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZeroToHeroReadmeDocumentsLifecycle(t *testing.T) {
|
||||
b, err := os.ReadFile("README.md")
|
||||
if err != nil {
|
||||
t.Fatalf("read README.md: %v", err)
|
||||
}
|
||||
doc := string(b)
|
||||
for _, want := range []string{
|
||||
"Scaffold services",
|
||||
"Run the harness",
|
||||
"Chat through an agent",
|
||||
"Inspect the workflow",
|
||||
"go test ./examples/support",
|
||||
} {
|
||||
if !strings.Contains(doc, want) {
|
||||
t.Fatalf("README.md missing zero-to-hero step %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestZeroToHeroInspectTranscript(t *testing.T) {
|
||||
out := captureStdout(t, func() {
|
||||
if err := runSupport("mock"); err != nil {
|
||||
t.Fatalf("support example failed: %v", err)
|
||||
}
|
||||
})
|
||||
got := stripANSI(out)
|
||||
|
||||
for _, want := range []string{
|
||||
`> event: events.ticket.created {"customer":"alice@acme.com","id":"ticket-1","subject":"Can't log in"}`,
|
||||
`[customers] looked up Alice (pro plan)`,
|
||||
`[tickets] ticket-1 → priority=high status=in_progress`,
|
||||
`approval gate notify_NotifyService_Send(alice@acme.com) — approved`,
|
||||
`[notify] 📨 to=alice@acme.com: "Hi Alice — thanks for reaching out. We've bumped this to high priority and are on it."`,
|
||||
`support agent: Triaged ticket-1 for Alice and sent a reply.`,
|
||||
`inspect transcript:`,
|
||||
`micro inspect flow intake`,
|
||||
`flow: intake runs=1 latest.reply="Triaged ticket-1 for Alice and sent a reply."`,
|
||||
`micro agent history support`,
|
||||
`agent: support runs=1 latest.status=completed`,
|
||||
`✓ ticket triaged and the customer was replied to — triggered by an event`,
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("support transcript missing %q\n--- got ---\n%s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
readme, err := os.ReadFile("README.md")
|
||||
if err != nil {
|
||||
t.Fatalf("read README.md: %v", err)
|
||||
}
|
||||
for _, want := range []string{
|
||||
"Expected inspect transcript",
|
||||
"micro inspect flow intake",
|
||||
"micro agent history support",
|
||||
"agent: support runs=1 latest.status=completed",
|
||||
} {
|
||||
if !strings.Contains(string(readme), want) {
|
||||
t.Fatalf("README.md missing transcript contract %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func captureStdout(t *testing.T, fn func()) (out string) {
|
||||
t.Helper()
|
||||
old := os.Stdout
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("capture stdout: %v", err)
|
||||
}
|
||||
os.Stdout = w
|
||||
|
||||
var buf bytes.Buffer
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
_, _ = io.Copy(&buf, r)
|
||||
close(done)
|
||||
}()
|
||||
defer func() {
|
||||
_ = w.Close()
|
||||
os.Stdout = old
|
||||
<-done
|
||||
out = buf.String()
|
||||
}()
|
||||
|
||||
fn()
|
||||
return out
|
||||
}
|
||||
|
||||
var ansiRE = regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
||||
|
||||
func stripANSI(s string) string {
|
||||
return ansiRE.ReplaceAllString(s, "")
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
# Web Service Example
|
||||
|
||||
HTTP web service with automatic service discovery and registration.
|
||||
|
||||
## What It Does
|
||||
|
||||
This example creates an HTTP service that:
|
||||
- Serves RESTful API endpoints
|
||||
- Registers with service discovery
|
||||
- Provides health checks
|
||||
- Uses standard Go HTTP handlers
|
||||
|
||||
## Run It
|
||||
|
||||
```bash
|
||||
go run main.go
|
||||
```
|
||||
|
||||
## Test It
|
||||
|
||||
```bash
|
||||
# Get service info
|
||||
curl http://localhost:9090/
|
||||
|
||||
# List all users
|
||||
curl http://localhost:9090/users
|
||||
|
||||
# Get specific user
|
||||
curl http://localhost:9090/users/1
|
||||
|
||||
# Health check
|
||||
curl http://localhost:9090/health
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Standard HTTP**: Use familiar `http.Handler` interface
|
||||
- **Service Discovery**: Automatically registers with registry
|
||||
- **Health Checks**: Built-in health endpoint
|
||||
- **JSON APIs**: Easy REST API development
|
||||
|
||||
## When to Use
|
||||
|
||||
Use `web.Service` when:
|
||||
- Building REST APIs
|
||||
- Serving web UIs
|
||||
- Working with HTTP-specific features
|
||||
- Migrating existing HTTP services
|
||||
|
||||
Use regular `micro.Service` when:
|
||||
- Building RPC services
|
||||
- Need bidirectional streaming
|
||||
- Want automatic load balancing
|
||||
- Prefer structured RPC over HTTP
|
||||
|
||||
## Next Steps
|
||||
|
||||
- See [hello-world](../hello-world/) for RPC services
|
||||
- See [production-ready](../production-ready/) for observability
|
||||
@@ -0,0 +1,70 @@
|
||||
module example
|
||||
|
||||
go 1.24
|
||||
|
||||
require go-micro.dev/v5 v5.16.0
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/bitly/go-simplejson v0.5.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||
github.com/cornelk/hashmap v1.0.8 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/consul/api v1.32.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-hclog v1.5.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/hashicorp/serf v0.10.1 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.50 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/hashstructure v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // 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
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rabbitmq/amqp091-go v1.10.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/urfave/cli/v2 v2.27.6 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
go.etcd.io/bbolt v1.4.0 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.5.21 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.5.21 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
golang.org/x/tools v0.31.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/grpc v1.71.1 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
)
|
||||
|
||||
replace go-micro.dev/v5 => ../..
|
||||
@@ -0,0 +1,385 @@
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
|
||||
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
|
||||
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cornelk/hashmap v1.0.8 h1:nv0AWgw02n+iDcawr5It4CjQIAcdMMKRrs10HOJYlrc=
|
||||
github.com/cornelk/hashmap v1.0.8/go.mod h1:RfZb7JO3RviW/rT6emczVuC/oxpdz4UsSB2LJSclR1k=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
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/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
|
||||
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
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.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/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/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
|
||||
github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
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/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE=
|
||||
github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4=
|
||||
github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg=
|
||||
github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
|
||||
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
|
||||
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
|
||||
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
|
||||
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
|
||||
github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM=
|
||||
github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
|
||||
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
|
||||
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
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-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
||||
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
||||
github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q=
|
||||
github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
|
||||
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
|
||||
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/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0=
|
||||
github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nats-io/jwt/v2 v2.7.4 h1:jXFuDDxs/GQjGDZGhNgH4tXzSUK6WQi2rsj4xmsNOtI=
|
||||
github.com/nats-io/jwt/v2 v2.7.4/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA=
|
||||
github.com/nats-io/nats-server/v2 v2.11.3 h1:AbGtXxuwjo0gBroLGGr/dE0vf24kTKdRnBq/3z/Fdoc=
|
||||
github.com/nats-io/nats-server/v2 v2.11.3/go.mod h1:6Z6Fd+JgckqzKig7DYwhgrE7bJ6fypPHnGPND+DqgMY=
|
||||
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/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
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/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE=
|
||||
github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
|
||||
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
|
||||
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
|
||||
go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8=
|
||||
go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs=
|
||||
go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY=
|
||||
go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU=
|
||||
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.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/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
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.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.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=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
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-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
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.4.2/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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/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.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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.0.0-20210220032951-036812b2e83c/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-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/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.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/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/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
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.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
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-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
|
||||
google.golang.org/grpc v1.71.1/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/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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=
|
||||
@@ -0,0 +1,102 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6/web"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
var users = map[string]*User{
|
||||
"1": {ID: "1", Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now()},
|
||||
"2": {ID: "2", Name: "Bob", Email: "bob@example.com", CreatedAt: time.Now()},
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Create a new web service
|
||||
service := web.NewService(
|
||||
web.Name("web.service"),
|
||||
web.Version("latest"),
|
||||
web.Address(":9090"),
|
||||
)
|
||||
|
||||
// Initialize
|
||||
service.Init()
|
||||
|
||||
// Register handlers
|
||||
service.HandleFunc("/", homeHandler)
|
||||
service.HandleFunc("/users", usersHandler)
|
||||
service.HandleFunc("/users/", userHandler)
|
||||
service.HandleFunc("/health", healthHandler)
|
||||
|
||||
fmt.Println("Web service starting on :9090")
|
||||
fmt.Println("Try:")
|
||||
fmt.Println(" curl http://localhost:9090/")
|
||||
fmt.Println(" curl http://localhost:9090/users")
|
||||
fmt.Println(" curl http://localhost:9090/users/1")
|
||||
fmt.Println(" curl http://localhost:9090/health")
|
||||
|
||||
// Run the service
|
||||
if err := service.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func homeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"service": "web.service",
|
||||
"version": "latest",
|
||||
"status": "running",
|
||||
})
|
||||
}
|
||||
|
||||
func usersHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// Return all users
|
||||
userList := make([]*User, 0, len(users))
|
||||
for _, user := range users {
|
||||
userList = append(userList, user)
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(userList)
|
||||
}
|
||||
|
||||
func userHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// Extract user ID from path
|
||||
id := r.URL.Path[len("/users/"):]
|
||||
|
||||
user, exists := users[id]
|
||||
if !exists {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": "User not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(user)
|
||||
}
|
||||
|
||||
func healthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "healthy",
|
||||
"timestamp": time.Now().Unix(),
|
||||
"uptime": "running",
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user