Compare commits

...

1 Commits

Author SHA1 Message Date
Shelley e993aec297 feat(run): integrate HTTP gateway with micro run
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
micro run now starts an HTTP gateway alongside your services:

  - Web dashboard at http://localhost:8080
  - API proxy at /api/{service}/{method}
  - Health checks at /health
  - Service listing at /services

The experience is now:
  $ micro new helloworld
  $ cd helloworld
  $ micro run

  Open http://localhost:8080 to see and call your services.

New flags:
  --address :3000    # Custom gateway port
  --no-gateway       # Disable gateway (services only)

Updated documentation to make this the central experience.

Co-authored-by: Shelley <shelley@exe.dev>
2026-01-27 12:22:23 +00:00
5 changed files with 477 additions and 37 deletions
+14 -8
View File
@@ -135,20 +135,29 @@ go install go-micro.dev/v5/cmd/micro@latest
```bash
micro new helloworld # Create a new service
cd helloworld
micro run # Run with hot reload
micro run # Run with API gateway
```
Then open http://localhost:8080 to see your service and call it from the browser.
### micro run
Run services with hot reload, dependency ordering, and environment management:
`micro run` starts your services with:
- **API Gateway** - HTTP to RPC proxy at `/api/{service}/{method}`
- **Web Dashboard** - Browse and call services at `/`
- **Health Checks** - Aggregated health at `/health`
- **Hot Reload** - Auto-rebuild on file changes
```bash
micro run # Hot reload enabled
micro run --no-watch # Disable hot reload
micro run # Gateway on :8080
micro run --address :3000 # Custom gateway port
micro run --no-gateway # Services only
micro run --env production # Use production environment
```
For multi-service projects, create a `micro.mu` configuration file:
### Configuration
For multi-service projects, create a `micro.mu` file:
```
service users
@@ -162,9 +171,6 @@ service api
env development
DATABASE_URL sqlite://./dev.db
env production
DATABASE_URL postgres://...
```
See [cmd/micro/README.md](cmd/micro/README.md) for full CLI documentation.
+41 -9
View File
@@ -27,28 +27,60 @@ This will:
## Run the service
Run the service with hot reload:
Run your service:
```
micro run
```
This will:
- Watch for file changes and auto-rebuild/restart
- Start services in dependency order (if configured)
- Apply environment-specific settings
This starts:
- **API Gateway** on http://localhost:8080
- **Web Dashboard** at http://localhost:8080
- **Hot Reload** watching for file changes
- **Services** in dependency order
Open http://localhost:8080 to see your services and call them from the browser.
### Output
Options:
```
micro run # Hot reload enabled (default)
┌─────────────────────────────────────────────────────────────┐
│ │
│ Micro │
│ │
│ Web: http://localhost:8080 │
│ API: http://localhost:8080/api/{service}/{method} │
│ Health: http://localhost:8080/health │
│ │
│ Services: │
│ ● helloworld │
│ │
│ Watching for changes... │
│ │
└─────────────────────────────────────────────────────────────┘
```
### Options
```
micro run # Gateway on :8080, hot reload enabled
micro run --address :3000 # Gateway on custom port
micro run --no-gateway # Services only, no HTTP gateway
micro run --no-watch # Disable hot reload
micro run --env production # Use production environment
micro run ./path/to/service # Run specific directory
micro run github.com/micro/blog # Clone and run from GitHub
```
List services to see it's running and registered itself:
### Calling Services
Via curl:
```bash
curl -X POST http://localhost:8080/api/helloworld/Helloworld.Call -d '{"name": "World"}'
```
Or browse to http://localhost:8080 and use the web interface.
List services:
```
micro services
```
+276
View File
@@ -0,0 +1,276 @@
// Package gateway provides an HTTP gateway for micro run
package gateway
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
"go-micro.dev/v5/client"
"go-micro.dev/v5/codec/bytes"
"go-micro.dev/v5/health"
"go-micro.dev/v5/registry"
)
// Gateway provides HTTP access to micro services
type Gateway struct {
addr string
server *http.Server
services []ServiceInfo
mu sync.RWMutex
}
// ServiceInfo holds information about a running service
type ServiceInfo struct {
Name string `json:"name"`
Address string `json:"address"`
Port int `json:"port,omitempty"`
}
// New creates a new gateway
func New(addr string) *Gateway {
return &Gateway{
addr: addr,
}
}
// SetServices updates the list of known services
func (g *Gateway) SetServices(services []ServiceInfo) {
g.mu.Lock()
g.services = services
g.mu.Unlock()
}
// Start starts the gateway HTTP server
func (g *Gateway) Start() error {
mux := http.NewServeMux()
// Health endpoint - aggregates all service health
mux.HandleFunc("/health", g.healthHandler)
mux.HandleFunc("/health/live", g.liveHandler)
mux.HandleFunc("/health/ready", g.readyHandler)
// API endpoint - HTTP to RPC proxy
mux.HandleFunc("/api/", g.apiHandler)
// Services list
mux.HandleFunc("/services", g.servicesHandler)
// Home page
mux.HandleFunc("/", g.homeHandler)
g.server = &http.Server{
Addr: g.addr,
Handler: mux,
}
go func() {
if err := g.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Printf("Gateway error: %v\n", err)
}
}()
return nil
}
// Stop stops the gateway
func (g *Gateway) Stop() {
if g.server != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
g.server.Shutdown(ctx)
}
}
// Addr returns the gateway address
func (g *Gateway) Addr() string {
return g.addr
}
func (g *Gateway) homeHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
g.mu.RLock()
services := g.services
g.mu.RUnlock()
// Get services from registry
regServices, _ := registry.ListServices()
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, `<!DOCTYPE html>
<html>
<head>
<title>Micro</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f5f5f5; color: #333; }
.container { max-width: 800px; margin: 0 auto; padding: 40px 20px; }
h1 { font-size: 2em; margin-bottom: 10px; }
.subtitle { color: #666; margin-bottom: 30px; }
.card { background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.card h2 { font-size: 1.2em; margin-bottom: 15px; color: #333; }
.service { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #eee; }
.service:last-child { border-bottom: none; }
.service-name { font-weight: 500; }
.service-addr { color: #666; font-family: monospace; font-size: 0.9em; }
.endpoints { margin-top: 10px; }
.endpoint { display: block; padding: 5px 10px; margin: 5px 0; background: #f0f0f0; border-radius: 4px; font-family: monospace; font-size: 0.85em; text-decoration: none; color: #333; }
.endpoint:hover { background: #e0e0e0; }
.try-it { background: #f9f9f9; padding: 15px; border-radius: 6px; margin-top: 20px; }
.try-it h3 { font-size: 1em; margin-bottom: 10px; }
code { background: #333; color: #0f0; padding: 10px 15px; display: block; border-radius: 4px; font-size: 0.85em; overflow-x: auto; }
.links { margin-top: 20px; }
.links a { color: #0066cc; margin-right: 15px; }
</style>
</head>
<body>
<div class="container">
<h1>Micro</h1>
<p class="subtitle">Services are running</p>
<div class="card">
<h2>Services (%d)</h2>
`, len(regServices))
if len(regServices) > 0 {
for _, svc := range regServices {
fmt.Fprintf(w, ` <div class="service">
<span class="service-name">%s</span>
</div>
`, svc.Name)
// Get endpoints for this service
if details, err := registry.GetService(svc.Name); err == nil && len(details) > 0 {
if len(details[0].Endpoints) > 0 {
fmt.Fprintf(w, ` <div class="endpoints">`)
for _, ep := range details[0].Endpoints {
fmt.Fprintf(w, ` <a class="endpoint" href="/api/%s/%s">POST /api/%s/%s</a>\n`,
svc.Name, ep.Name, svc.Name, ep.Name)
}
fmt.Fprintf(w, ` </div>`)
}
}
}
} else if len(services) > 0 {
for _, svc := range services {
fmt.Fprintf(w, ` <div class="service">
<span class="service-name">%s</span>
<span class="service-addr">%s</span>
</div>
`, svc.Name, svc.Address)
}
} else {
fmt.Fprintf(w, ` <p style="color: #666; padding: 10px 0;">No services registered yet...</p>`)
}
fmt.Fprintf(w, ` </div>
<div class="card">
<h2>Quick Links</h2>
<div class="links">
<a href="/health">Health Check</a>
<a href="/services">Services JSON</a>
</div>
</div>
<div class="try-it">
<h3>Try it</h3>
<code>curl -X POST http://localhost%s/api/{service}/{Endpoint} -d '{}'</code>
</div>
</div>
</body>
</html>`, g.addr)
}
func (g *Gateway) servicesHandler(w http.ResponseWriter, r *http.Request) {
services, err := registry.ListServices()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
var result []map[string]interface{}
for _, svc := range services {
details, _ := registry.GetService(svc.Name)
var endpoints []string
if len(details) > 0 {
for _, ep := range details[0].Endpoints {
endpoints = append(endpoints, ep.Name)
}
}
result = append(result, map[string]interface{}{
"name": svc.Name,
"endpoints": endpoints,
})
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
func (g *Gateway) healthHandler(w http.ResponseWriter, r *http.Request) {
resp := health.Run(r.Context())
w.Header().Set("Content-Type", "application/json")
if resp.Status == health.StatusUp {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusServiceUnavailable)
}
json.NewEncoder(w).Encode(resp)
}
func (g *Gateway) liveHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"up"}`))
}
func (g *Gateway) readyHandler(w http.ResponseWriter, r *http.Request) {
g.healthHandler(w, r)
}
func (g *Gateway) apiHandler(w http.ResponseWriter, r *http.Request) {
// Parse path: /api/{service}/{endpoint}
path := strings.TrimPrefix(r.URL.Path, "/api/")
parts := strings.SplitN(path, "/", 2)
if len(parts) < 2 {
http.Error(w, `{"error": "usage: /api/{service}/{endpoint}"}`, http.StatusBadRequest)
return
}
service := parts[0]
endpoint := parts[1]
// Read request body
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, fmt.Sprintf(`{"error": "%s"}`, err.Error()), http.StatusBadRequest)
return
}
if len(body) == 0 {
body = []byte("{}")
}
// Create RPC request
req := client.NewRequest(service, endpoint, &bytes.Frame{Data: body})
var rsp bytes.Frame
if err := client.Call(r.Context(), req, &rsp); err != nil {
http.Error(w, fmt.Sprintf(`{"error": "%s"}`, err.Error()), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(rsp.Data)
}
+97 -8
View File
@@ -19,6 +19,7 @@ import (
"github.com/urfave/cli/v2"
"go-micro.dev/v5/cmd"
"go-micro.dev/v5/cmd/micro/run/config"
"go-micro.dev/v5/cmd/micro/run/gateway"
"go-micro.dev/v5/cmd/micro/run/watcher"
)
@@ -318,8 +319,29 @@ func Run(c *cli.Context) error {
return fmt.Errorf("no services found")
}
// Start gateway unless disabled
var gw *gateway.Gateway
gatewayAddr := c.String("address")
if gatewayAddr == "" {
gatewayAddr = ":8080"
}
if !c.Bool("no-gateway") {
gw = gateway.New(gatewayAddr)
var svcInfos []gateway.ServiceInfo
for _, svc := range services {
svcInfos = append(svcInfos, gateway.ServiceInfo{
Name: svc.name,
Port: svc.port,
})
}
gw.SetServices(svcInfos)
if err := gw.Start(); err != nil {
return fmt.Errorf("failed to start gateway: %w", err)
}
}
// Start services
fmt.Printf("Starting %d service(s)...\n", len(services))
for _, svc := range services {
if err := svc.start(logsDir); err != nil {
fmt.Fprintf(os.Stderr, "[%s] %v\n", svc.name, err)
@@ -334,6 +356,9 @@ func Run(c *cli.Context) error {
}
}
// Print startup banner
printBanner(services, gw, !c.Bool("no-watch"))
// Setup signal handling
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
@@ -350,7 +375,6 @@ func Run(c *cli.Context) error {
watch = watcher.New(dirs)
watch.Start()
fmt.Println("Watching for changes... (use --no-watch to disable)")
go func() {
for event := range watch.Events() {
@@ -372,6 +396,10 @@ func Run(c *cli.Context) error {
watch.Stop()
}
if gw != nil {
gw.Stop()
}
// Stop services in reverse order
for i := len(services) - 1; i >= 0; i-- {
services[i].stop()
@@ -398,23 +426,84 @@ func processRunning(pidStr string) bool {
return proc.Signal(syscall.Signal(0)) == nil
}
func printBanner(services []*serviceProcess, gw *gateway.Gateway, watching bool) {
fmt.Println()
fmt.Println(" ┌─────────────────────────────────────────────────────────────┐")
fmt.Println(" │ │")
fmt.Println(" │ \033[1mMicro\033[0m │")
fmt.Println(" │ │")
if gw != nil {
fmt.Printf(" │ Web: \033[36mhttp://localhost%s\033[0m │\n", gw.Addr())
fmt.Printf(" │ API: \033[36mhttp://localhost%s/api/{service}/{method}\033[0m │\n", gw.Addr())
fmt.Printf(" │ Health: \033[36mhttp://localhost%s/health\033[0m │\n", gw.Addr())
}
fmt.Println(" │ │")
fmt.Println(" │ Services: │")
for _, svc := range services {
status := "\033[32m●\033[0m" // green dot
if !svc.running {
status = "\033[31m●\033[0m" // red dot
}
name := svc.name
if len(name) > 20 {
name = name[:17] + "..."
}
fmt.Printf(" │ %s %-20s │\n", status, name)
}
fmt.Println(" │ │")
if watching {
fmt.Println(" │ \033[33mWatching for changes...\033[0m │")
fmt.Println(" │ │")
}
if gw != nil && len(services) > 0 {
svc := services[0]
fmt.Println(" │ Try: │")
fmt.Printf(" │ \033[90mcurl -X POST http://localhost%s/api/%s/...\033[0m │\n", gw.Addr(), svc.name)
fmt.Println(" │ │")
}
fmt.Println(" └─────────────────────────────────────────────────────────────┘")
fmt.Println()
}
func init() {
cmd.Register(&cli.Command{
Name: "run",
Usage: "Run services with hot reload",
Usage: "Run services with API gateway and hot reload",
Description: `Run discovers and runs services in a directory.
Starts an HTTP gateway on :8080 providing:
- Web dashboard at /
- API proxy at /api/{service}/{endpoint}
- Health checks at /health
With a micro.mu or micro.json config file, services start in dependency order.
Without config, all main.go files are discovered and run.
Examples:
micro run # Run services in current directory with hot reload
micro run ./myapp # Run services in ./myapp
micro run --no-watch # Run without hot reload
micro run --env production # Use production environment
micro run github.com/micro/blog # Clone and run`,
micro run # Run with gateway on :8080
micro run --address :3000 # Gateway on custom port
micro run --no-gateway # Services only, no HTTP gateway
micro run --no-watch # Disable hot reload
micro run --env production # Use production environment`,
Action: Run,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "address",
Aliases: []string{"a"},
Usage: "Gateway address (default :8080)",
Value: ":8080",
},
&cli.BoolFlag{
Name: "no-gateway",
Usage: "Disable HTTP gateway",
},
&cli.BoolFlag{
Name: "no-watch",
Usage: "Disable hot reload (file watching)",
+49 -12
View File
@@ -4,23 +4,49 @@ layout: default
# micro run - Local Development
`micro run` provides a Rails/Spring-like development experience for Go microservices.
`micro run` provides a complete development environment for Go microservices.
## Quick Start
```bash
# Run services in current directory with hot reload
micro new helloworld
cd helloworld
micro run
# Run from a specific directory
micro run ./myapp
# Clone and run from GitHub
micro run github.com/micro/blog
```
Open http://localhost:8080 to see your service.
## What You Get
When you run `micro run`, you get:
| URL | Description |
|-----|-------------|
| http://localhost:8080 | Web dashboard - browse and call services |
| http://localhost:8080/api/{service}/{method} | API gateway - HTTP to RPC proxy |
| http://localhost:8080/health | Health checks - aggregated service health |
| http://localhost:8080/services | Service list - JSON |
Plus:
- **Hot Reload** - File changes trigger automatic rebuild
- **Dependency Ordering** - Services start in the right order
- **Environment Management** - Dev/staging/production configs
## Features
### API Gateway
The gateway converts HTTP requests to RPC calls:
```bash
# Call a service method
curl -X POST http://localhost:8080/api/helloworld/Say.Hello \
-d '{"name": "World"}'
# Response
{"message": "Hello World"}
```
### Hot Reload
By default, `micro run` watches for `.go` file changes and automatically rebuilds and restarts affected services.
@@ -183,9 +209,20 @@ Run it:
micro run github.com/micro/blog
```
## Options
```bash
micro run # Gateway on :8080, hot reload
micro run --address :3000 # Custom gateway port
micro run --no-gateway # Services only, no HTTP gateway
micro run --no-watch # Disable hot reload
micro run --env production # Use production environment
```
## Tips
1. **Port Configuration**: Set `port` for services that expose HTTP to enable health check waiting
2. **Health Endpoint**: Implement `/health` returning 200 for reliable startup sequencing
3. **Environment Separation**: Keep secrets in production env, use file:// paths for development
4. **Hot Reload Scope**: Only `.go` files trigger rebuilds; static assets don't
1. **Browse First**: Open http://localhost:8080 to explore your services
2. **Port Configuration**: Set `port` for services to enable health check waiting
3. **Health Endpoint**: Implement `/health` returning 200 for reliable startup sequencing
4. **Environment Separation**: Keep secrets in production env, use file:// paths for development
5. **Hot Reload Scope**: Only `.go` files trigger rebuilds; static assets don't