Compare commits

..

1 Commits

Author SHA1 Message Date
Shelley 93f605035e feat: add testing package for in-process service testing
Run Tests / Unit Tests (push) Waiting to run
Run Tests / Etcd Integration Tests (push) Waiting to run
Provides a test harness for running micro services in isolation:

  h := testing.NewHarness(t)
  defer h.Stop()

  h.Name("users").Register(new(UsersHandler))
  h.Start()

  var rsp Response
  h.Call("UsersHandler.Create", &req, &rsp)

Features:
- Isolated registry, transport, and broker per harness
- Simple API: Name(), Register(), Start(), Stop()
- Call helpers: Call(), CallContext()
- Assertions: AssertServiceRunning(), AssertCallSucceeds(), AssertCallFails()
- Access to underlying Client(), Server(), Registry()

Note: Due to go-micro's global defaults, each harness tests one service.
For multi-service testing, use integration tests or mocks.

Also fixes README.md example showing conflicting port 8080.

Co-authored-by: Shelley <shelley@exe.dev>
2026-01-27 12:34:44 +00:00
52 changed files with 397 additions and 5738 deletions
+2 -37
View File
@@ -117,11 +117,9 @@ There's a new `genai` package for generative AI capabilities.
Install the code generator and see usage in the docs:
```bash
go install go-micro.dev/v5/cmd/protoc-gen-micro@v5.13.0
go install go-micro.dev/v5/cmd/protoc-gen-micro@latest
```
> **Note:** Use a specific version instead of `@latest` to avoid module path conflicts. See [releases](https://github.com/micro/go-micro/releases) for the latest version.
Docs: [`internal/website/docs/getting-started.md`](internal/website/docs/getting-started.md)
## Command Line
@@ -129,11 +127,9 @@ Docs: [`internal/website/docs/getting-started.md`](internal/website/docs/getting
Install the CLI:
```
go install go-micro.dev/v5/cmd/micro@v5.13.0
go install go-micro.dev/v5/cmd/micro@latest
```
> **Note:** Use a specific version instead of `@latest` to avoid module path conflicts. See [releases](https://github.com/micro/go-micro/releases) for the latest version.
### Quick Start
```bash
@@ -179,36 +175,6 @@ env development
The gateway runs on :8080 by default, so services should use other ports.
### Deployment
Deploy to any Linux server with systemd:
```bash
# On your server (one-time setup)
curl -fsSL https://go-micro.dev/install.sh | sh
sudo micro init --server
# From your laptop
micro deploy user@your-server
```
The deploy command:
1. Builds binaries for Linux
2. Copies via SSH to the server
3. Sets up systemd services
4. Verifies services are healthy
Manage deployed services:
```bash
micro status --remote user@server # Check status
micro logs --remote user@server # View logs
micro logs myservice --remote user@server -f # Follow specific service
```
No Docker required. No Kubernetes. Just systemd.
See [docs/deployment.md](docs/deployment.md) for full deployment guide.
See [cmd/micro/README.md](cmd/micro/README.md) for full CLI documentation.
Docs: [`internal/website/docs`](internal/website/docs)
@@ -219,7 +185,6 @@ Selected topics:
- Getting Started: [`internal/website/docs/getting-started.md`](internal/website/docs/getting-started.md)
- Plugins overview: [`internal/website/docs/plugins.md`](internal/website/docs/plugins.md)
- Learn by Example: [`internal/website/docs/examples/index.md`](internal/website/docs/examples/index.md)
- Performance Considerations: [`docs/performance.md`](docs/performance.md)
## Adopters
+14 -127
View File
@@ -6,7 +6,6 @@ import (
"errors"
"strings"
"sync"
"time"
natsp "github.com/nats-io/nats.go"
"go-micro.dev/v5/broker"
@@ -23,15 +22,10 @@ type natsBroker struct {
connected bool
addrs []string
conn *natsp.Conn // single connection (used when pool is disabled)
pool *connectionPool // connection pool (used when pooling is enabled)
conn *natsp.Conn
opts broker.Options
nopts natsp.Options
// pool configuration
poolSize int
poolIdleTimeout time.Duration
// should we drain the connection
drain bool
closeCh chan (error)
@@ -115,39 +109,6 @@ func (n *natsBroker) Connect() error {
return nil
}
// Check if we should use connection pooling
if n.poolSize > 1 {
// Initialize connection pool
factory := func() (*natsp.Conn, error) {
opts := n.nopts
opts.Servers = n.addrs
opts.Secure = n.opts.Secure
opts.TLSConfig = n.opts.TLSConfig
// secure might not be set
if n.opts.TLSConfig != nil {
opts.Secure = true
}
return opts.Connect()
}
pool, err := newConnectionPool(n.poolSize, factory)
if err != nil {
return err
}
// Set idle timeout if configured
if n.poolIdleTimeout > 0 {
pool.idleTimeout = n.poolIdleTimeout
}
n.pool = pool
n.connected = true
return nil
}
// Single connection mode (original behavior)
status := natsp.CLOSED
if n.conn != nil {
status = n.conn.Status()
@@ -182,26 +143,14 @@ func (n *natsBroker) Disconnect() error {
n.Lock()
defer n.Unlock()
// Close connection pool if it exists
if n.pool != nil {
if err := n.pool.Close(); err != nil {
n.opts.Logger.Log(logger.ErrorLevel, "error closing connection pool:", err)
}
n.pool = nil
// drain the connection if specified
if n.drain {
n.conn.Drain()
n.closeCh <- nil
}
// Close single connection if it exists
if n.conn != nil {
// drain the connection if specified
if n.drain {
n.conn.Drain()
n.closeCh <- nil
}
// close the client connection
n.conn.Close()
n.conn = nil
}
// close the client connection
n.conn.Close()
// set not connected
n.connected = false
@@ -222,42 +171,24 @@ func (n *natsBroker) Publish(topic string, msg *broker.Message, opts ...broker.P
n.RLock()
defer n.RUnlock()
b, err := n.opts.Codec.Marshal(msg)
if err != nil {
return err
}
// Use connection pool if enabled
if n.pool != nil {
poolConn, err := n.pool.Get()
if err != nil {
return err
}
defer n.pool.Put(poolConn)
conn := poolConn.Conn()
if conn == nil {
return errors.New("invalid connection from pool")
}
return conn.Publish(topic, b)
}
// Use single connection (original behavior)
if n.conn == nil {
return errors.New("not connected")
}
b, err := n.opts.Codec.Marshal(msg)
if err != nil {
return err
}
return n.conn.Publish(topic, b)
}
func (n *natsBroker) Subscribe(topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
n.RLock()
hasConnection := n.conn != nil || n.pool != nil
n.RUnlock()
if !hasConnection {
if n.conn == nil {
n.RUnlock()
return nil, errors.New("not connected")
}
n.RUnlock()
opt := broker.SubscribeOptions{
AutoAck: true,
@@ -295,38 +226,6 @@ func (n *natsBroker) Subscribe(topic string, handler broker.Handler, opts ...bro
var sub *natsp.Subscription
var err error
// Use connection pool if enabled
if n.pool != nil {
poolConn, err := n.pool.Get()
if err != nil {
return nil, err
}
conn := poolConn.Conn()
if conn == nil {
n.pool.Put(poolConn)
return nil, errors.New("invalid connection from pool")
}
if len(opt.Queue) > 0 {
sub, err = conn.QueueSubscribe(topic, opt.Queue, fn)
} else {
sub, err = conn.Subscribe(topic, fn)
}
if err != nil {
n.pool.Put(poolConn)
return nil, err
}
// Return connection to pool after subscription is created
// The subscription keeps the connection alive
n.pool.Put(poolConn)
return &subscriber{s: sub, opts: opt}, nil
}
// Use single connection (original behavior)
n.RLock()
if len(opt.Queue) > 0 {
sub, err = n.conn.QueueSubscribe(topic, opt.Queue, fn)
@@ -351,24 +250,12 @@ func (n *natsBroker) setOption(opts ...broker.Option) {
n.Once.Do(func() {
n.nopts = natsp.GetDefaultOptions()
n.poolSize = 1 // Default to single connection (no pooling)
n.poolIdleTimeout = 5 * time.Minute
})
if nopts, ok := n.opts.Context.Value(optionsKey{}).(natsp.Options); ok {
n.nopts = nopts
}
// Set pool size if configured
if poolSize, ok := n.opts.Context.Value(poolSizeKey{}).(int); ok && poolSize > 0 {
n.poolSize = poolSize
}
// Set pool idle timeout if configured
if idleTimeout, ok := n.opts.Context.Value(poolIdleTimeoutKey{}).(time.Duration); ok {
n.poolIdleTimeout = idleTimeout
}
// broker.Options have higher priority than nats.Options
// only if Addrs, Secure or TLSConfig were not set through a broker.Option
// we read them from nats.Option
-18
View File
@@ -1,16 +1,12 @@
package nats
import (
"time"
natsp "github.com/nats-io/nats.go"
"go-micro.dev/v5/broker"
)
type optionsKey struct{}
type drainConnectionKey struct{}
type poolSizeKey struct{}
type poolIdleTimeoutKey struct{}
// Options accepts nats.Options.
func Options(opts natsp.Options) broker.Option {
@@ -21,17 +17,3 @@ func Options(opts natsp.Options) broker.Option {
func DrainConnection() broker.Option {
return setBrokerOption(drainConnectionKey{}, struct{}{})
}
// PoolSize sets the size of the connection pool.
// If set to a value > 1, the broker will use a connection pool.
// Default is 1 (no pooling).
func PoolSize(size int) broker.Option {
return setBrokerOption(poolSizeKey{}, size)
}
// PoolIdleTimeout sets the timeout for idle connections in the pool.
// Connections idle for longer than this duration will be closed.
// Default is 5 minutes. Set to 0 to disable idle timeout.
func PoolIdleTimeout(timeout time.Duration) broker.Option {
return setBrokerOption(poolIdleTimeoutKey{}, timeout)
}
-188
View File
@@ -1,188 +0,0 @@
package nats
import (
"errors"
"sync"
"time"
natsp "github.com/nats-io/nats.go"
)
var (
// ErrPoolExhausted is returned when no connections are available in the pool
ErrPoolExhausted = errors.New("connection pool exhausted")
// ErrPoolClosed is returned when trying to use a closed pool
ErrPoolClosed = errors.New("connection pool is closed")
)
// connectionPool manages a pool of NATS connections
type connectionPool struct {
mu sync.RWMutex
connections chan *pooledConnection
factory func() (*natsp.Conn, error)
size int
idleTimeout time.Duration
closed bool
}
// pooledConnection wraps a NATS connection with metadata
type pooledConnection struct {
conn *natsp.Conn
createdAt time.Time
lastUsed time.Time
mu sync.Mutex
}
// newConnectionPool creates a new connection pool
func newConnectionPool(size int, factory func() (*natsp.Conn, error)) (*connectionPool, error) {
if size <= 0 {
size = 1
}
pool := &connectionPool{
connections: make(chan *pooledConnection, size),
factory: factory,
size: size,
idleTimeout: 5 * time.Minute,
closed: false,
}
return pool, nil
}
// Get retrieves a connection from the pool or creates a new one
func (p *connectionPool) Get() (*pooledConnection, error) {
p.mu.RLock()
if p.closed {
p.mu.RUnlock()
return nil, ErrPoolClosed
}
p.mu.RUnlock()
// Try to get an existing connection from the pool
select {
case conn := <-p.connections:
// Check if connection is still valid and not idle for too long
if conn.isValid() && !conn.isExpired(p.idleTimeout) {
conn.updateLastUsed()
return conn, nil
}
// Connection is invalid or expired, close it and create a new one
conn.close()
return p.createConnection()
default:
// No connection available, create a new one
return p.createConnection()
}
}
// Put returns a connection to the pool
func (p *connectionPool) Put(conn *pooledConnection) error {
p.mu.RLock()
defer p.mu.RUnlock()
if p.closed {
return conn.close()
}
// Check if connection is still valid
if !conn.isValid() {
return conn.close()
}
conn.updateLastUsed()
// Try to return connection to pool
select {
case p.connections <- conn:
return nil
default:
// Pool is full, close the connection
return conn.close()
}
}
// Close closes all connections in the pool
func (p *connectionPool) Close() error {
p.mu.Lock()
defer p.mu.Unlock()
if p.closed {
return nil
}
p.closed = true
close(p.connections)
// Close all connections in the pool
for conn := range p.connections {
conn.close()
}
return nil
}
// createConnection creates a new pooled connection
func (p *connectionPool) createConnection() (*pooledConnection, error) {
conn, err := p.factory()
if err != nil {
return nil, err
}
return &pooledConnection{
conn: conn,
createdAt: time.Now(),
lastUsed: time.Now(),
}, nil
}
// isValid checks if the underlying NATS connection is valid
func (pc *pooledConnection) isValid() bool {
pc.mu.Lock()
defer pc.mu.Unlock()
if pc.conn == nil {
return false
}
status := pc.conn.Status()
return status == natsp.CONNECTED || status == natsp.RECONNECTING
}
// isExpired checks if the connection has been idle for too long
func (pc *pooledConnection) isExpired(timeout time.Duration) bool {
pc.mu.Lock()
defer pc.mu.Unlock()
if timeout <= 0 {
return false
}
return time.Since(pc.lastUsed) > timeout
}
// close closes the underlying NATS connection
func (pc *pooledConnection) close() error {
pc.mu.Lock()
defer pc.mu.Unlock()
if pc.conn != nil {
pc.conn.Close()
pc.conn = nil
}
return nil
}
// Conn returns the underlying NATS connection
func (pc *pooledConnection) Conn() *natsp.Conn {
pc.mu.Lock()
defer pc.mu.Unlock()
return pc.conn
}
// updateLastUsed updates the last used timestamp in a thread-safe manner
func (pc *pooledConnection) updateLastUsed() {
pc.mu.Lock()
defer pc.mu.Unlock()
pc.lastUsed = time.Now()
}
-204
View File
@@ -1,204 +0,0 @@
package nats
import (
"sync"
"testing"
"time"
natsp "github.com/nats-io/nats.go"
)
func TestConnectionPool_GetPut(t *testing.T) {
// Mock factory that creates connections
connCount := 0
factory := func() (*natsp.Conn, error) {
connCount++
// Return a mock connection (we can't create real NATS connections in tests without a server)
// This test is more about the pool logic
return nil, nil
}
pool, err := newConnectionPool(3, factory)
if err != nil {
t.Fatalf("Failed to create pool: %v", err)
}
defer pool.Close()
// Get a connection (should create one)
conn1, err := pool.Get()
if err != nil {
t.Fatalf("Failed to get connection: %v", err)
}
if conn1 == nil {
t.Fatal("Expected connection, got nil")
}
// Put it back
if err := pool.Put(conn1); err != nil {
t.Fatalf("Failed to put connection: %v", err)
}
// Get it again (should reuse the same one)
conn2, err := pool.Get()
if err != nil {
t.Fatalf("Failed to get connection: %v", err)
}
// Since we can't compare actual connections easily, just verify we got one
if conn2 == nil {
t.Fatal("Expected connection, got nil")
}
}
func TestConnectionPool_Concurrent(t *testing.T) {
connCount := 0
mu := sync.Mutex{}
factory := func() (*natsp.Conn, error) {
mu.Lock()
connCount++
mu.Unlock()
return nil, nil
}
pool, err := newConnectionPool(5, factory)
if err != nil {
t.Fatalf("Failed to create pool: %v", err)
}
defer pool.Close()
// Simulate concurrent access
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
conn, err := pool.Get()
if err != nil {
t.Errorf("Failed to get connection: %v", err)
return
}
// Simulate some work
time.Sleep(10 * time.Millisecond)
if err := pool.Put(conn); err != nil {
t.Errorf("Failed to put connection: %v", err)
}
}()
}
wg.Wait()
// We should have created some connections
mu.Lock()
if connCount == 0 {
t.Error("Expected at least one connection to be created")
}
mu.Unlock()
}
func TestConnectionPool_Close(t *testing.T) {
factory := func() (*natsp.Conn, error) {
return nil, nil
}
pool, err := newConnectionPool(3, factory)
if err != nil {
t.Fatalf("Failed to create pool: %v", err)
}
// Get a connection
conn, err := pool.Get()
if err != nil {
t.Fatalf("Failed to get connection: %v", err)
}
// Close the pool
if err := pool.Close(); err != nil {
t.Fatalf("Failed to close pool: %v", err)
}
// Put connection back to closed pool should not panic
// The connection will be closed instead of returned to pool
_ = pool.Put(conn)
// Try to get from closed pool
_, err = pool.Get()
if err != ErrPoolClosed {
t.Errorf("Expected ErrPoolClosed, got: %v", err)
}
}
func TestPooledConnection_IsValid(t *testing.T) {
pc := &pooledConnection{
conn: nil, // nil connection should be invalid
createdAt: time.Now(),
lastUsed: time.Now(),
}
if pc.isValid() {
t.Error("Expected nil connection to be invalid")
}
}
func TestPooledConnection_IsExpired(t *testing.T) {
pc := &pooledConnection{
conn: nil,
createdAt: time.Now(),
lastUsed: time.Now().Add(-10 * time.Minute), // 10 minutes ago
}
// With 5 minute timeout, should be expired
if !pc.isExpired(5 * time.Minute) {
t.Error("Expected connection to be expired")
}
// With 0 timeout, should never expire
if pc.isExpired(0) {
t.Error("Expected connection not to expire with 0 timeout")
}
// With 20 minute timeout, should not be expired
if pc.isExpired(20 * time.Minute) {
t.Error("Expected connection not to be expired")
}
}
func TestNatsBroker_PoolConfiguration(t *testing.T) {
// Test that pool size is set correctly
br := NewNatsBroker(PoolSize(5))
nb, ok := br.(*natsBroker)
if !ok {
t.Fatal("Expected broker to be of type *natsBroker")
}
if nb.poolSize != 5 {
t.Errorf("Expected pool size 5, got %d", nb.poolSize)
}
// Test with custom idle timeout
br2 := NewNatsBroker(PoolSize(3), PoolIdleTimeout(10*time.Minute))
nb2, ok := br2.(*natsBroker)
if !ok {
t.Fatal("Expected broker to be of type *natsBroker")
}
if nb2.poolSize != 3 {
t.Errorf("Expected pool size 3, got %d", nb2.poolSize)
}
if nb2.poolIdleTimeout != 10*time.Minute {
t.Errorf("Expected idle timeout 10m, got %v", nb2.poolIdleTimeout)
}
}
func TestNatsBroker_DefaultSingleConnection(t *testing.T) {
// Test that default behavior is single connection (pool size 1)
br := NewNatsBroker()
nb, ok := br.(*natsBroker)
if !ok {
t.Fatal("Expected broker to be of type *natsBroker")
}
if nb.poolSize != 1 {
t.Errorf("Expected default pool size 1, got %d", nb.poolSize)
}
}
-68
View File
@@ -272,74 +272,6 @@ func main() {
}
```
## Building and Deployment
### Build Binaries
Build Go binaries for deployment:
```bash
micro build # Build for current OS
micro build --os linux # Cross-compile for Linux
micro build --os linux --arch arm64 # For ARM64
micro build --output ./dist # Custom output directory
```
### Deploy to Server
Deploy to any Linux server with systemd:
```bash
# First time: set up the server
ssh user@server
curl -fsSL https://go-micro.dev/install.sh | sh
sudo micro init --server
exit
# Deploy from your laptop
micro deploy user@server
```
The deploy command:
1. Builds binaries for linux/amd64
2. Copies via SSH to `/opt/micro/bin/`
3. Sets up systemd services (`micro@<service>`)
4. Restarts and verifies services are running
### Named Deploy Targets
Add deploy targets to `micro.mu`:
```
deploy prod
ssh deploy@prod.example.com
deploy staging
ssh deploy@staging.example.com
```
Then:
```bash
micro deploy prod # Deploy to production
micro deploy staging # Deploy to staging
```
### Managing Deployed Services
```bash
# Check status
micro status --remote user@server
# View logs
micro logs --remote user@server
micro logs myservice --remote user@server -f
# Stop a service
micro stop myservice --remote user@server
```
See [docs/deployment.md](../../docs/deployment.md) for the full deployment guide.
## Protobuf
Use protobuf for code generation with [protoc-gen-micro](https://github.com/micro/go-micro/tree/master/cmd/protoc-gen-micro)
+1 -3
View File
@@ -13,11 +13,9 @@ write services. Surrounding this we introduce a number of tools to make it easy
Install `micro` via `go install`
```
go install go-micro.dev/v5/cmd/micro@v5.13.0
go install go-micro.dev/v5@latest
```
> **Note:** Use a specific version instead of `@latest` to avoid module path conflicts. See [releases](https://github.com/micro/go-micro/releases) for the latest version.
Or via install script
```
-343
View File
@@ -1,343 +0,0 @@
// Package build provides the micro build command for building service binaries
package build
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/cmd"
"go-micro.dev/v5/cmd/micro/run/config"
)
// Build builds Go binaries for services
func Build(c *cli.Context) error {
dir := c.Args().Get(0)
if dir == "" {
dir = "."
}
absDir, err := filepath.Abs(dir)
if err != nil {
return fmt.Errorf("failed to get absolute path: %w", err)
}
// Load config
cfg, err := config.Load(absDir)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
// Output directory
outDir := c.String("output")
if outDir == "" {
outDir = filepath.Join(absDir, "bin")
}
if err := os.MkdirAll(outDir, 0755); err != nil {
return fmt.Errorf("failed to create output dir: %w", err)
}
// Target OS/ARCH
targetOS := c.String("os")
targetArch := c.String("arch")
if targetOS == "" {
targetOS = runtime.GOOS
}
if targetArch == "" {
targetArch = runtime.GOARCH
}
if cfg != nil && len(cfg.Services) > 0 {
// Build each service from config
sorted, err := cfg.TopologicalSort()
if err != nil {
return err
}
for _, svc := range sorted {
svcDir := filepath.Join(absDir, svc.Path)
if err := buildService(svc.Name, svcDir, outDir, targetOS, targetArch); err != nil {
return fmt.Errorf("failed to build %s: %w", svc.Name, err)
}
}
} else {
// Build single service from current directory
name := filepath.Base(absDir)
if err := buildService(name, absDir, outDir, targetOS, targetArch); err != nil {
return err
}
}
fmt.Printf("\n✓ Built to %s\n", outDir)
return nil
}
func buildService(name, dir, outDir, targetOS, targetArch string) error {
binName := name
if targetOS == "windows" {
binName += ".exe"
}
outPath := filepath.Join(outDir, binName)
fmt.Printf("Building %s (%s/%s)...\n", name, targetOS, targetArch)
// Build command
buildCmd := exec.Command("go", "build", "-o", outPath, ".")
buildCmd.Dir = dir
buildCmd.Env = append(os.Environ(),
"GOOS="+targetOS,
"GOARCH="+targetArch,
"CGO_ENABLED=0",
)
buildCmd.Stdout = os.Stdout
buildCmd.Stderr = os.Stderr
if err := buildCmd.Run(); err != nil {
return fmt.Errorf("go build failed: %w", err)
}
fmt.Printf("✓ %s\n", outPath)
return nil
}
// Docker builds container images (optional)
func Docker(c *cli.Context) error {
dir := c.Args().Get(0)
if dir == "" {
dir = "."
}
absDir, err := filepath.Abs(dir)
if err != nil {
return fmt.Errorf("failed to get absolute path: %w", err)
}
cfg, err := config.Load(absDir)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
tag := c.String("tag")
if tag == "" {
tag = "latest"
}
registry := c.String("registry")
push := c.Bool("push")
if cfg != nil && len(cfg.Services) > 0 {
for name, svc := range cfg.Services {
svcDir := filepath.Join(absDir, svc.Path)
if err := buildDockerImage(name, svcDir, svc.Port, tag, registry, push); err != nil {
return fmt.Errorf("failed to build %s: %w", name, err)
}
}
} else {
name := filepath.Base(absDir)
if err := buildDockerImage(name, absDir, 8080, tag, registry, push); err != nil {
return err
}
}
return nil
}
const dockerfileTemplate = `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:latest
RUN apk --no-cache add ca-certificates
COPY --from=builder /service /service
EXPOSE %d
CMD ["/service"]
`
func buildDockerImage(name, dir string, port int, tag, registry string, push bool) error {
if port == 0 {
port = 8080
}
// Generate Dockerfile if not exists
dockerfilePath := filepath.Join(dir, "Dockerfile")
if _, err := os.Stat(dockerfilePath); os.IsNotExist(err) {
fmt.Printf("Generating Dockerfile for %s...\n", name)
dockerfile := fmt.Sprintf(dockerfileTemplate, port)
if err := os.WriteFile(dockerfilePath, []byte(dockerfile), 0644); err != nil {
return fmt.Errorf("failed to write Dockerfile: %w", err)
}
}
imageName := name + ":" + tag
if registry != "" {
imageName = registry + "/" + imageName
}
fmt.Printf("Building %s...\n", imageName)
buildCmd := exec.Command("docker", "build", "-t", imageName, dir)
buildCmd.Stdout = os.Stdout
buildCmd.Stderr = os.Stderr
if err := buildCmd.Run(); err != nil {
return fmt.Errorf("docker build failed: %w", err)
}
fmt.Printf("✓ Built %s\n", imageName)
if push {
fmt.Printf("Pushing %s...\n", imageName)
pushCmd := exec.Command("docker", "push", imageName)
pushCmd.Stdout = os.Stdout
pushCmd.Stderr = os.Stderr
if err := pushCmd.Run(); err != nil {
return fmt.Errorf("docker push failed: %w", err)
}
fmt.Printf("✓ Pushed %s\n", imageName)
}
return nil
}
// Compose generates docker-compose.yml (optional)
func Compose(c *cli.Context) error {
dir := c.Args().Get(0)
if dir == "" {
dir = "."
}
absDir, err := filepath.Abs(dir)
if err != nil {
return fmt.Errorf("failed to get absolute path: %w", err)
}
cfg, err := config.Load(absDir)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
if cfg == nil || len(cfg.Services) == 0 {
return fmt.Errorf("no services found in micro.mu or micro.json")
}
registry := c.String("registry")
tag := c.String("tag")
if tag == "" {
tag = "latest"
}
var sb strings.Builder
sb.WriteString("# Generated by micro build --compose\n")
sb.WriteString("version: '3.8'\n\nservices:\n")
sorted, err := cfg.TopologicalSort()
if err != nil {
return err
}
for _, svc := range sorted {
imageName := svc.Name + ":" + tag
if registry != "" {
imageName = registry + "/" + imageName
}
sb.WriteString(fmt.Sprintf(" %s:\n", svc.Name))
sb.WriteString(fmt.Sprintf(" image: %s\n", imageName))
if svc.Port > 0 {
sb.WriteString(fmt.Sprintf(" ports:\n - \"%d:%d\"\n", svc.Port, svc.Port))
}
if len(svc.Depends) > 0 {
sb.WriteString(" depends_on:\n")
for _, dep := range svc.Depends {
sb.WriteString(fmt.Sprintf(" - %s\n", dep))
}
}
sb.WriteString(" environment:\n - MICRO_REGISTRY=mdns\n\n")
}
output := filepath.Join(absDir, "docker-compose.yml")
if err := os.WriteFile(output, []byte(sb.String()), 0644); err != nil {
return fmt.Errorf("failed to write docker-compose.yml: %w", err)
}
fmt.Printf("✓ Generated %s\n", output)
return nil
}
func init() {
cmd.Register(&cli.Command{
Name: "build",
Usage: "Build Go binaries for services",
Description: `Build compiles Go binaries for your services.
With a micro.mu config, builds all services. Without, builds the current directory.
Output goes to ./bin/ by default.
Examples:
micro build # Build for current OS/arch
micro build --os linux # Cross-compile for Linux
micro build --os linux --arch arm64 # For ARM64
micro build --output ./dist # Custom output directory
Docker (optional):
micro build --docker # Build container images
micro build --docker --push # Build and push
micro build --compose # Generate docker-compose.yml`,
Action: func(c *cli.Context) error {
if c.Bool("docker") {
return Docker(c)
}
if c.Bool("compose") {
return Compose(c)
}
return Build(c)
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Output directory (default: ./bin)",
},
&cli.StringFlag{
Name: "os",
Usage: "Target OS (linux, darwin, windows)",
},
&cli.StringFlag{
Name: "arch",
Usage: "Target architecture (amd64, arm64)",
},
// Docker options (optional)
&cli.BoolFlag{
Name: "docker",
Usage: "Build Docker container images instead",
},
&cli.StringFlag{
Name: "tag",
Aliases: []string{"t"},
Usage: "Docker image tag (default: latest)",
Value: "latest",
},
&cli.StringFlag{
Name: "registry",
Aliases: []string{"r"},
Usage: "Docker registry (e.g., docker.io/myuser)",
},
&cli.BoolFlag{
Name: "push",
Usage: "Push Docker images after building",
},
&cli.BoolFlag{
Name: "compose",
Usage: "Generate docker-compose.yml",
},
},
})
}
+190 -11
View File
@@ -1,11 +1,16 @@
package microcli
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"syscall"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/client"
@@ -16,12 +21,6 @@ import (
"go-micro.dev/v5/cmd/micro/cli/new"
"go-micro.dev/v5/cmd/micro/cli/util"
// Import packages that register commands via init()
_ "go-micro.dev/v5/cmd/micro/cli/build"
_ "go-micro.dev/v5/cmd/micro/cli/deploy"
_ "go-micro.dev/v5/cmd/micro/cli/init"
_ "go-micro.dev/v5/cmd/micro/cli/remote"
)
var (
@@ -58,6 +57,55 @@ func genTextHandler(c *cli.Context) error {
return nil
}
func lastNonEmptyLine(s string) string {
lines := strings.Split(s, "\n")
for i := len(lines) - 1; i >= 0; i-- {
if strings.TrimSpace(lines[i]) != "" {
return lines[i]
}
}
return ""
}
func lastLogLine(path string) string {
f, err := os.Open(path)
if err != nil {
return ""
}
defer f.Close()
var last string
scan := bufio.NewScanner(f)
for scan.Scan() {
if strings.TrimSpace(scan.Text()) != "" {
last = scan.Text()
}
}
return last
}
func waitAndCleanup(procs []*exec.Cmd, pidFiles []string) {
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt)
go func() {
<-ch
for _, proc := range procs {
if proc.Process != nil {
_ = proc.Process.Kill()
}
}
for _, pf := range pidFiles {
_ = os.Remove(pf)
}
os.Exit(1)
}()
for i, proc := range procs {
_ = proc.Wait()
if proc.Process != nil {
_ = os.Remove(pidFiles[i])
}
}
}
func init() {
cmd.Register([]*cli.Command{
{
@@ -154,11 +202,142 @@ func init() {
return nil
},
},
// Note: The following commands are registered in their respective packages:
// - status, logs, stop: remote/remote.go
// - build: build/build.go
// - deploy: deploy/deploy.go
// - init: init/init.go
{
Name: "status",
Usage: "Check status of running services",
Action: func(ctx *cli.Context) error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home dir: %w", err)
}
runDir := filepath.Join(homeDir, "micro", "run")
files, err := os.ReadDir(runDir)
if err != nil {
return fmt.Errorf("failed to read run dir: %w", err)
}
fmt.Printf("%-20s %-8s %-8s %s\n", "SERVICE", "PID", "STATUS", "DIRECTORY")
for _, f := range files {
if f.IsDir() || !strings.HasSuffix(f.Name(), ".pid") {
continue
}
service := f.Name()[:len(f.Name())-4]
pidFilePath := filepath.Join(runDir, f.Name())
pidFile, err := os.Open(pidFilePath)
if err != nil {
continue
}
var pid int
var dir string
scanner := bufio.NewScanner(pidFile)
if scanner.Scan() {
fmt.Sscanf(scanner.Text(), "%d", &pid)
}
if scanner.Scan() {
dir = scanner.Text()
}
pidFile.Close()
status := "stopped"
if pid > 0 {
proc, err := os.FindProcess(pid)
if err == nil {
if err := proc.Signal(syscall.Signal(0)); err == nil {
status = "running"
}
}
}
fmt.Printf("%-20s %-8d %-8s %-40s %s\n", service, pid, status, "", dir)
}
return nil
},
},
{
Name: "stop",
Usage: "Stop a running service",
Action: func(ctx *cli.Context) error {
if ctx.Args().Len() != 1 {
return fmt.Errorf("Usage: micro stop [service]")
}
service := ctx.Args().Get(0)
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home dir: %w", err)
}
runDir := filepath.Join(homeDir, "micro", "run")
pidFilePath := filepath.Join(runDir, service+".pid")
pidFile, err := os.Open(pidFilePath)
if err != nil {
return fmt.Errorf("no pid file for service %s", service)
}
var pid int
var dir string
scanner := bufio.NewScanner(pidFile)
if scanner.Scan() {
fmt.Sscanf(scanner.Text(), "%d", &pid)
}
if scanner.Scan() {
dir = scanner.Text()
}
pidFile.Close()
if pid <= 0 {
_ = os.Remove(pidFilePath)
return fmt.Errorf("service %s is not running", service)
}
proc, err := os.FindProcess(pid)
if err != nil {
_ = os.Remove(pidFilePath)
return fmt.Errorf("could not find process for %s", service)
}
if err := proc.Signal(syscall.SIGTERM); err != nil {
_ = os.Remove(pidFilePath)
return fmt.Errorf("failed to stop service %s: %v", service, err)
}
_ = os.Remove(pidFilePath)
fmt.Printf("Stopped service %s (pid %d) in directory %s\n", service, pid, dir)
return nil
},
},
{
Name: "logs",
Usage: "Show logs for a service, or list available logs if no service is specified",
Action: func(ctx *cli.Context) error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home dir: %w", err)
}
logsDir := filepath.Join(homeDir, "micro", "logs")
if ctx.Args().Len() == 0 {
// List available logs
dirEntries, err := os.ReadDir(logsDir)
if err != nil {
return fmt.Errorf("could not list logs directory: %v", err)
}
fmt.Println("Available logs:")
found := false
for _, entry := range dirEntries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".log") {
fmt.Println(" ", strings.TrimSuffix(entry.Name(), ".log"))
found = true
}
}
if !found {
fmt.Println(" (no logs found)")
}
return nil
}
service := ctx.Args().Get(0)
logFilePath := filepath.Join(logsDir, service+".log")
f, err := os.Open(logFilePath)
if err != nil {
return fmt.Errorf("could not open log file for service %s: %v", service, err)
}
defer f.Close()
scan := bufio.NewScanner(f)
for scan.Scan() {
fmt.Println(scan.Text())
}
return scan.Err()
},
},
}...)
cmd.App().Action = func(c *cli.Context) error {
-447
View File
@@ -1,447 +0,0 @@
// Package deploy provides the micro deploy command for deploying services
package deploy
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/cmd"
"go-micro.dev/v5/cmd/micro/run/config"
)
const (
defaultRemotePath = "/opt/micro"
)
// Deploy deploys services to a target
func Deploy(c *cli.Context) error {
// Get target from args or flag
target := c.Args().First()
if target == "" {
target = c.String("ssh")
}
// Load config to check for deploy targets
dir := "."
absDir, _ := filepath.Abs(dir)
cfg, _ := config.Load(absDir)
// If still no target, check config for named targets
if target == "" && cfg != nil && len(cfg.Deploy) > 0 {
// Show available targets
return showDeployTargets(cfg)
}
if target == "" {
return showDeployHelp()
}
// Check if target is a named target from config
if cfg != nil {
if dt, ok := cfg.Deploy[target]; ok {
target = dt.SSH
}
}
return deploySSH(c, target, cfg)
}
func showDeployHelp() error {
return fmt.Errorf(`No deployment target specified.
To deploy, you need a server running micro. Quick setup:
1. On your server (Ubuntu/Debian):
ssh user@your-server
curl -fsSL https://go-micro.dev/install.sh | sh
sudo micro init --server
2. Then deploy from here:
micro deploy user@your-server
Or add to micro.mu:
deploy prod
ssh user@your-server
Run 'micro deploy --help' for more options.`)
}
func showDeployTargets(cfg *config.Config) error {
var sb strings.Builder
sb.WriteString("Available deploy targets:\n\n")
for name, dt := range cfg.Deploy {
sb.WriteString(fmt.Sprintf(" %s -> %s\n", name, dt.SSH))
}
sb.WriteString("\nDeploy with: micro deploy <target>")
return fmt.Errorf("%s", sb.String())
}
func deploySSH(c *cli.Context, target string, cfg *config.Config) error {
dir := c.Args().Get(1)
if dir == "" {
dir = "."
}
absDir, err := filepath.Abs(dir)
if err != nil {
return fmt.Errorf("failed to get absolute path: %w", err)
}
// Load config if not passed
if cfg == nil {
cfg, _ = config.Load(absDir)
}
remotePath := c.String("path")
if remotePath == "" {
remotePath = defaultRemotePath
}
fmt.Printf("Deploying to %s...\n\n", target)
// Step 1: Check SSH connectivity
fmt.Print(" Checking SSH connection... ")
if err := checkSSH(target); err != nil {
fmt.Println("\u2717")
return err
}
fmt.Println("\u2713")
// Step 2: Check server is initialized
fmt.Print(" Checking server setup... ")
if err := checkServerInit(target, remotePath); err != nil {
fmt.Println("\u2717")
return err
}
fmt.Println("\u2713")
// Step 3: Build binaries
var services []string
if cfg != nil && len(cfg.Services) > 0 {
sorted, err := cfg.TopologicalSort()
if err != nil {
return err
}
for _, svc := range sorted {
services = append(services, svc.Name)
}
} else {
services = []string{filepath.Base(absDir)}
}
fmt.Printf(" Building binaries... ")
if err := buildBinaries(absDir, cfg, c.Bool("build")); err != nil {
fmt.Println("\u2717")
return err
}
fmt.Printf("\u2713 %s\n", strings.Join(services, ", "))
// Step 4: Copy binaries
fmt.Printf(" Copying binaries... ")
if err := copyBinaries(target, filepath.Join(absDir, "bin"), remotePath); err != nil {
fmt.Println("\u2717")
return err
}
fmt.Printf("\u2713 %d services\n", len(services))
// Step 5: Setup and restart services via systemd
fmt.Printf(" Updating systemd... ")
if err := setupSystemdServices(target, remotePath, services); err != nil {
fmt.Println("\u2717")
return err
}
fmt.Printf("\u2713 %s\n", strings.Join(prefixServices(services), ", "))
// Step 6: Restart services
fmt.Printf(" Restarting services... ")
if err := restartServices(target, services); err != nil {
fmt.Println("\u2717")
return err
}
fmt.Println("\u2713")
// Step 7: Check health
fmt.Printf(" Checking health... ")
time.Sleep(2 * time.Second) // Give services time to start
healthy, unhealthy := checkServicesHealth(target, services)
if len(unhealthy) > 0 {
fmt.Printf("\u26a0 %d/%d healthy\n", len(healthy), len(services))
} else {
fmt.Println("\u2713 all healthy")
}
fmt.Println()
fmt.Printf("\u2713 Deployed to %s\n", target)
fmt.Println()
fmt.Printf(" Status: micro status --remote %s\n", target)
fmt.Printf(" Logs: micro logs --remote %s\n", target)
if len(unhealthy) > 0 {
fmt.Println()
fmt.Printf("\u26a0 Some services may have issues: %s\n", strings.Join(unhealthy, ", "))
fmt.Printf(" Check logs: micro logs %s --remote %s\n", unhealthy[0], target)
}
return nil
}
func prefixServices(services []string) []string {
result := make([]string, len(services))
for i, s := range services {
result[i] = "micro@" + s
}
return result
}
func checkSSH(host string) error {
testCmd := exec.Command("ssh", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", host, "echo ok")
output, err := testCmd.CombinedOutput()
if err != nil {
return fmt.Errorf(`
\u2717 Cannot connect to %s
SSH connection failed. Check that:
\u2022 The server is reachable: ping %s
\u2022 SSH is configured: ssh %s
\u2022 Your key is added: ssh-add -l
Common fixes:
\u2022 Add SSH key: ssh-copy-id %s
\u2022 Check hostname in ~/.ssh/config
Error: %s`, host, host, host, host, strings.TrimSpace(string(output)))
}
return nil
}
func checkServerInit(host, remotePath string) error {
checkCmd := fmt.Sprintf("test -f %s/.micro-initialized", remotePath)
sshCmd := exec.Command("ssh", host, checkCmd)
if err := sshCmd.Run(); err != nil {
return fmt.Errorf(`
\u2717 Server not initialized
micro is not set up on %s.
Run this on the server:
ssh %s
curl -fsSL https://go-micro.dev/install.sh | sh
sudo micro init --server
Or initialize remotely (requires sudo):
micro init --server --remote %s`, host, host, host)
}
return nil
}
func buildBinaries(absDir string, cfg *config.Config, forceBuild bool) error {
binDir := filepath.Join(absDir, "bin")
// Check if we already have binaries and don't need to rebuild
if !forceBuild {
if _, err := os.Stat(binDir); err == nil {
// Check if binaries are for linux
// For now, just rebuild to be safe
}
}
// Always build for linux/amd64
targetOS := "linux"
targetArch := "amd64"
if err := os.MkdirAll(binDir, 0755); err != nil {
return err
}
if cfg != nil && len(cfg.Services) > 0 {
sorted, err := cfg.TopologicalSort()
if err != nil {
return err
}
for _, svc := range sorted {
svcDir := filepath.Join(absDir, svc.Path)
outPath := filepath.Join(binDir, svc.Name)
buildCmd := exec.Command("go", "build", "-o", outPath, ".")
buildCmd.Dir = svcDir
buildCmd.Env = append(os.Environ(),
"GOOS="+targetOS,
"GOARCH="+targetArch,
"CGO_ENABLED=0",
)
if output, err := buildCmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to build %s:\n%s", svc.Name, string(output))
}
}
} else {
name := filepath.Base(absDir)
outPath := filepath.Join(binDir, name)
buildCmd := exec.Command("go", "build", "-o", outPath, ".")
buildCmd.Dir = absDir
buildCmd.Env = append(os.Environ(),
"GOOS="+targetOS,
"GOARCH="+targetArch,
"CGO_ENABLED=0",
)
if output, err := buildCmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to build:\n%s", string(output))
}
}
return nil
}
func copyBinaries(target, binDir, remotePath string) error {
// Ensure remote bin directory exists
mkdirCmd := exec.Command("ssh", target, fmt.Sprintf("mkdir -p %s/bin", remotePath))
if err := mkdirCmd.Run(); err != nil {
return fmt.Errorf("failed to create remote directory: %w", err)
}
// Use rsync for efficient copy
// --omit-dir-times avoids permission errors on directory timestamps
rsyncArgs := []string{
"-avz", "--delete", "--omit-dir-times",
binDir + "/",
fmt.Sprintf("%s:%s/bin/", target, remotePath),
}
rsyncCmd := exec.Command("rsync", rsyncArgs...)
output, err := rsyncCmd.CombinedOutput()
if err != nil {
outputStr := string(output)
// Fall back to scp if rsync not available
if strings.Contains(outputStr, "command not found") {
scpCmd := exec.Command("scp", "-r", binDir+"/", fmt.Sprintf("%s:%s/bin/", target, remotePath))
if scpOutput, scpErr := scpCmd.CombinedOutput(); scpErr != nil {
return fmt.Errorf("copy failed: %s", string(scpOutput))
}
return nil
}
// rsync exit code 23 means some files failed to transfer, but if we see our files listed, it's ok
// rsync exit code 24 means some files vanished during transfer (harmless)
exitErr, ok := err.(*exec.ExitError)
if ok && (exitErr.ExitCode() == 23 || exitErr.ExitCode() == 24) {
// Check if it's just permission warnings on metadata, not actual file transfer failures
if !strings.Contains(outputStr, "Permission denied (13)") ||
strings.Contains(outputStr, "failed to set times") ||
strings.Contains(outputStr, "chgrp") {
// These are acceptable warnings
return nil
}
}
return fmt.Errorf("copy failed: %s", outputStr)
}
return nil
}
func setupSystemdServices(target, remotePath string, services []string) error {
for _, svc := range services {
// Enable the service using the template
enableCmd := fmt.Sprintf("sudo systemctl enable micro@%s 2>/dev/null || true", svc)
sshCmd := exec.Command("ssh", target, enableCmd)
sshCmd.Run() // Ignore errors, service might already be enabled
}
// Reload systemd
reloadCmd := exec.Command("ssh", target, "sudo systemctl daemon-reload")
if err := reloadCmd.Run(); err != nil {
return fmt.Errorf("failed to reload systemd: %w", err)
}
return nil
}
func restartServices(target string, services []string) error {
for _, svc := range services {
restartCmd := fmt.Sprintf("sudo systemctl restart micro@%s", svc)
sshCmd := exec.Command("ssh", target, restartCmd)
if output, err := sshCmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to restart %s: %s", svc, string(output))
}
}
return nil
}
func checkServicesHealth(target string, services []string) (healthy, unhealthy []string) {
for _, svc := range services {
checkCmd := fmt.Sprintf("systemctl is-active micro@%s", svc)
sshCmd := exec.Command("ssh", target, checkCmd)
if err := sshCmd.Run(); err != nil {
unhealthy = append(unhealthy, svc)
} else {
healthy = append(healthy, svc)
}
}
return
}
// Ensure we're not on Windows for deploy
func checkPlatform() error {
if runtime.GOOS == "windows" {
return fmt.Errorf("micro deploy requires SSH and rsync, which work best on Linux/macOS.\nConsider using WSL on Windows.")
}
return nil
}
func init() {
cmd.Register(&cli.Command{
Name: "deploy",
Usage: "Deploy services to a remote server",
Description: `Deploy copies binaries to a remote server and manages them with systemd.
Before deploying, initialize the server:
ssh user@server 'curl -fsSL https://go-micro.dev/install.sh | sh && sudo micro init --server'
Then deploy:
micro deploy user@server
With a micro.mu config, you can define named targets:
deploy prod
ssh user@prod.example.com
deploy staging
ssh user@staging.example.com
Then: micro deploy prod
The deploy process:
1. Builds binaries for linux/amd64
2. Copies to /opt/micro/bin/ via rsync
3. Enables and restarts systemd services
4. Verifies services are healthy`,
Action: func(c *cli.Context) error {
if err := checkPlatform(); err != nil {
return err
}
return Deploy(c)
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "ssh",
Usage: "Deploy target as user@host (can also be positional arg)",
},
&cli.StringFlag{
Name: "path",
Usage: "Remote path (default: /opt/micro)",
Value: "/opt/micro",
},
&cli.BoolFlag{
Name: "build",
Usage: "Force rebuild of binaries",
},
},
})
}
-269
View File
@@ -1,269 +0,0 @@
// Package initcmd provides the micro init command for server setup
package initcmd
import (
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strings"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/cmd"
)
const systemdTemplate = `[Unit]
Description=Micro service: %%i
After=network.target
[Service]
Type=simple
User=%s
Group=%s
WorkingDirectory=%s
ExecStart=%s/bin/%%i
Restart=on-failure
RestartSec=5
EnvironmentFile=-%s/config/%%i.env
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=micro-%%i
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=%s/data
[Install]
WantedBy=multi-user.target
`
// Init initializes a server to receive micro deployments
func Init(c *cli.Context) error {
if !c.Bool("server") {
return fmt.Errorf("usage: micro init --server\n\nInitialize this machine to receive micro deployments")
}
// Check if we're on Linux
if runtime.GOOS != "linux" {
return fmt.Errorf("micro init --server is only supported on Linux")
}
// Check for remote init
remoteHost := c.String("remote")
if remoteHost != "" {
return initRemote(c, remoteHost)
}
basePath := c.String("path")
userName := c.String("user")
fmt.Println("Initializing micro server...")
fmt.Println()
// Check if running as root (needed for systemd and creating users)
if os.Geteuid() != 0 {
return fmt.Errorf(`micro init --server requires root privileges.
Run with sudo:
sudo micro init --server`)
}
// Create user if needed
if userName == "micro" {
if err := createMicroUser(); err != nil {
return err
}
}
// Create directories
fmt.Println("Creating directories:")
dirs := []string{
filepath.Join(basePath, "bin"),
filepath.Join(basePath, "data"),
filepath.Join(basePath, "config"),
}
for _, dir := range dirs {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create %s: %w", dir, err)
}
fmt.Printf(" ✓ %s\n", dir)
}
// Set ownership
if userName != "root" {
u, err := user.Lookup(userName)
if err != nil {
return fmt.Errorf("user %s not found: %w", userName, err)
}
// chown -R user:user /opt/micro
chownCmd := exec.Command("chown", "-R", fmt.Sprintf("%s:%s", u.Username, u.Username), basePath)
if err := chownCmd.Run(); err != nil {
return fmt.Errorf("failed to set ownership: %w", err)
}
}
fmt.Println()
// Create systemd template
fmt.Println("Creating systemd template:")
unitContent := fmt.Sprintf(systemdTemplate, userName, userName, basePath, basePath, basePath, basePath)
unitPath := "/etc/systemd/system/micro@.service"
if err := os.WriteFile(unitPath, []byte(unitContent), 0644); err != nil {
return fmt.Errorf("failed to write systemd unit: %w", err)
}
fmt.Printf(" ✓ %s\n", unitPath)
// Reload systemd
reloadCmd := exec.Command("systemctl", "daemon-reload")
if err := reloadCmd.Run(); err != nil {
return fmt.Errorf("failed to reload systemd: %w", err)
}
fmt.Println(" ✓ systemd daemon-reload")
// Write marker file so deploy can detect initialization
markerPath := filepath.Join(basePath, ".micro-initialized")
if err := os.WriteFile(markerPath, []byte("1\n"), 0644); err != nil {
return fmt.Errorf("failed to write marker: %w", err)
}
fmt.Println()
fmt.Println("Server ready!")
fmt.Println()
fmt.Println(" Deploy from your machine:")
fmt.Printf(" micro deploy user@%s\n", getHostname())
fmt.Println()
fmt.Println(" Manage services:")
fmt.Println(" sudo systemctl status micro@myservice")
fmt.Println(" sudo journalctl -u micro@myservice -f")
fmt.Println()
return nil
}
func createMicroUser() error {
// Check if user exists
if _, err := user.Lookup("micro"); err == nil {
return nil // user already exists
}
fmt.Println("Creating micro user:")
createCmd := exec.Command("useradd", "--system", "--no-create-home", "--shell", "/bin/false", "micro")
if err := createCmd.Run(); err != nil {
// Check if it's just because user already exists
if _, lookupErr := user.Lookup("micro"); lookupErr == nil {
return nil
}
return fmt.Errorf("failed to create micro user: %w", err)
}
fmt.Println(" ✓ Created user 'micro'")
return nil
}
func initRemote(c *cli.Context, host string) error {
fmt.Printf("Initializing micro on %s...\n\n", host)
// Check SSH connectivity first
if err := checkSSH(host); err != nil {
return err
}
basePath := c.String("path")
userName := c.String("user")
// Run micro init --server on remote
initCmd := fmt.Sprintf("sudo micro init --server --path %s --user %s", basePath, userName)
sshCmd := exec.Command("ssh", host, initCmd)
sshCmd.Stdout = os.Stdout
sshCmd.Stderr = os.Stderr
if err := sshCmd.Run(); err != nil {
return fmt.Errorf("remote init failed: %w", err)
}
return nil
}
func checkSSH(host string) error {
// Quick SSH test
testCmd := exec.Command("ssh", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", host, "echo ok")
output, err := testCmd.CombinedOutput()
if err != nil {
return fmt.Errorf(`✗ Cannot connect to %s
SSH connection failed. Check that:
• The server is reachable: ping %s
• SSH is configured: ssh %s
• Your key is added: ssh-add -l
Common fixes:
• Add SSH key: ssh-copy-id %s
• Check hostname in ~/.ssh/config
Error: %s`, host, host, host, host, strings.TrimSpace(string(output)))
}
return nil
}
func getHostname() string {
name, err := os.Hostname()
if err != nil {
return "this-server"
}
return name
}
func init() {
cmd.Register(&cli.Command{
Name: "init",
Usage: "Initialize micro for development or server deployment",
Description: `Initialize micro on a server to receive deployments.
Server setup:
sudo micro init --server
This creates:
• /opt/micro/bin/ - service binaries
• /opt/micro/data/ - persistent data
• /opt/micro/config/ - environment files
• systemd template for managing services
Remote setup:
micro init --server --remote user@host
After init, deploy with:
micro deploy user@host`,
Action: Init,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "server",
Usage: "Initialize as a deployment server",
},
&cli.StringFlag{
Name: "path",
Usage: "Base path for micro (default: /opt/micro)",
Value: "/opt/micro",
},
&cli.StringFlag{
Name: "user",
Usage: "User to run services as (default: micro)",
Value: "micro",
},
&cli.StringFlag{
Name: "remote",
Usage: "Initialize a remote server via SSH",
},
},
})
}
-367
View File
@@ -1,367 +0,0 @@
// Package remote provides remote server operations for micro
package remote
import (
"bufio"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/cmd"
)
const defaultRemotePath = "/opt/micro"
// Status shows status of services (local or remote)
func Status(c *cli.Context) error {
remoteHost := c.String("remote")
if remoteHost != "" {
return remoteStatus(remoteHost)
}
return localStatus(c)
}
func localStatus(c *cli.Context) error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home dir: %w", err)
}
runDir := filepath.Join(homeDir, "micro", "run")
files, err := os.ReadDir(runDir)
if err != nil {
fmt.Println("No services running locally.")
fmt.Println("\nStart services with: micro run")
return nil
}
var hasServices bool
fmt.Printf("%-20s %-10s %-8s %s\n", "SERVICE", "STATUS", "PID", "DIRECTORY")
fmt.Println(strings.Repeat("-", 70))
for _, f := range files {
if f.IsDir() || !strings.HasSuffix(f.Name(), ".pid") {
continue
}
hasServices = true
service := f.Name()[:len(f.Name())-4]
pidFilePath := filepath.Join(runDir, f.Name())
pidFile, err := os.Open(pidFilePath)
if err != nil {
continue
}
var pid int
var dir string
scanner := bufio.NewScanner(pidFile)
if scanner.Scan() {
fmt.Sscanf(scanner.Text(), "%d", &pid)
}
if scanner.Scan() {
dir = scanner.Text()
}
pidFile.Close()
status := "\u2717 stopped"
if pid > 0 {
proc, err := os.FindProcess(pid)
if err == nil {
if err := proc.Signal(syscall.Signal(0)); err == nil {
status = "\u25cf running"
}
}
}
fmt.Printf("%-20s %-10s %-8d %s\n", service, status, pid, dir)
}
if !hasServices {
fmt.Println("No services running locally.")
fmt.Println("\nStart services with: micro run")
}
return nil
}
func remoteStatus(host string) error {
// Get list of micro services via systemctl
listCmd := exec.Command("ssh", host, "systemctl list-units 'micro@*' --no-legend --no-pager 2>/dev/null || true")
output, err := listCmd.Output()
if err != nil {
return fmt.Errorf("failed to get status from %s: %w", host, err)
}
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
if len(lines) == 0 || (len(lines) == 1 && lines[0] == "") {
fmt.Printf("%s\n", host)
fmt.Println(strings.Repeat("\u2501", 50))
fmt.Println("\nNo services deployed.")
fmt.Println("\nDeploy with: micro deploy " + host)
return nil
}
fmt.Printf("%s\n", host)
fmt.Println(strings.Repeat("\u2501", 50))
fmt.Println()
for _, line := range lines {
if line == "" {
continue
}
parts := strings.Fields(line)
if len(parts) < 4 {
continue
}
unit := parts[0]
loadState := parts[1]
activeState := parts[2]
subState := parts[3]
// Extract service name from micro@servicename.service
serviceName := strings.TrimPrefix(unit, "micro@")
serviceName = strings.TrimSuffix(serviceName, ".service")
// Get more details
statusIcon := "\u25cf"
statusText := subState
if activeState != "active" || subState != "running" {
statusIcon = "\u2717"
}
_ = loadState // unused but parsed
fmt.Printf(" %-15s %s %s\n", serviceName, statusIcon, statusText)
}
fmt.Println()
return nil
}
// Logs shows logs for services (local or remote)
func Logs(c *cli.Context) error {
remoteHost := c.String("remote")
service := c.Args().First()
follow := c.Bool("follow") || c.Bool("f")
lines := c.Int("lines")
if remoteHost != "" {
return remoteLogs(remoteHost, service, follow, lines)
}
return localLogs(c, service, follow, lines)
}
func localLogs(c *cli.Context, service string, follow bool, lines int) error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home dir: %w", err)
}
logDir := filepath.Join(homeDir, "micro", "logs")
if service == "" {
// List available logs
files, err := os.ReadDir(logDir)
if err != nil {
fmt.Println("No logs available.")
return nil
}
fmt.Println("Available logs:")
for _, f := range files {
if strings.HasSuffix(f.Name(), ".log") {
name := strings.TrimSuffix(f.Name(), ".log")
fmt.Printf(" %s\n", name)
}
}
fmt.Println("\nView logs: micro logs <service>")
return nil
}
logPath := filepath.Join(logDir, service+".log")
if _, err := os.Stat(logPath); os.IsNotExist(err) {
return fmt.Errorf("no logs for service '%s'", service)
}
if follow {
cmd := exec.Command("tail", "-f", logPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
if lines == 0 {
lines = 100
}
cmd := exec.Command("tail", "-n", fmt.Sprintf("%d", lines), logPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func remoteLogs(host, service string, follow bool, lines int) error {
var journalCmd string
if service == "" {
// All micro services
journalCmd = "journalctl -u 'micro@*'"
} else {
journalCmd = fmt.Sprintf("journalctl -u 'micro@%s'", service)
}
if follow {
journalCmd += " -f"
} else {
if lines == 0 {
lines = 100
}
journalCmd += fmt.Sprintf(" -n %d", lines)
}
journalCmd += " --no-pager"
sshCmd := exec.Command("ssh", host, journalCmd)
sshCmd.Stdout = os.Stdout
sshCmd.Stderr = os.Stderr
return sshCmd.Run()
}
// Stop stops a running service
func Stop(c *cli.Context) error {
if c.Args().Len() != 1 {
return fmt.Errorf("Usage: micro stop <service>")
}
service := c.Args().First()
remoteHost := c.String("remote")
if remoteHost != "" {
return remoteStop(remoteHost, service)
}
return localStop(service)
}
func localStop(service string) error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home dir: %w", err)
}
runDir := filepath.Join(homeDir, "micro", "run")
pidFilePath := filepath.Join(runDir, service+".pid")
pidFile, err := os.Open(pidFilePath)
if err != nil {
return fmt.Errorf("service '%s' is not running", service)
}
var pid int
scanner := bufio.NewScanner(pidFile)
if scanner.Scan() {
fmt.Sscanf(scanner.Text(), "%d", &pid)
}
pidFile.Close()
if pid <= 0 {
_ = os.Remove(pidFilePath)
return fmt.Errorf("service '%s' is not running", service)
}
proc, err := os.FindProcess(pid)
if err != nil {
_ = os.Remove(pidFilePath)
return fmt.Errorf("could not find process for '%s'", service)
}
if err := proc.Signal(syscall.SIGTERM); err != nil {
_ = os.Remove(pidFilePath)
return fmt.Errorf("failed to stop service '%s': %v", service, err)
}
_ = os.Remove(pidFilePath)
fmt.Printf("Stopped %s (pid %d)\n", service, pid)
return nil
}
func remoteStop(host, service string) error {
stopCmd := fmt.Sprintf("sudo systemctl stop micro@%s", service)
sshCmd := exec.Command("ssh", host, stopCmd)
if output, err := sshCmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to stop %s: %s", service, string(output))
}
fmt.Printf("Stopped %s on %s\n", service, host)
return nil
}
func init() {
cmd.Register(&cli.Command{
Name: "status",
Usage: "Check status of running services",
Description: `Show status of running services.
Local status:
micro status
Remote status:
micro status --remote user@host`,
Action: Status,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "remote",
Usage: "Check status on remote server",
},
},
})
cmd.Register(&cli.Command{
Name: "logs",
Usage: "Show logs for a service",
Description: `View service logs.
Local logs:
micro logs # list available logs
micro logs myservice # show logs for myservice
micro logs myservice -f # follow logs
Remote logs:
micro logs --remote user@host
micro logs myservice --remote user@host -f`,
Action: Logs,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "remote",
Usage: "View logs on remote server",
},
&cli.BoolFlag{
Name: "follow",
Aliases: []string{"f"},
Usage: "Follow log output",
},
&cli.IntFlag{
Name: "lines",
Aliases: []string{"n"},
Usage: "Number of lines to show (default: 100)",
Value: 100,
},
},
})
cmd.Register(&cli.Command{
Name: "stop",
Usage: "Stop a running service",
Description: `Stop a running service.
Local:
micro stop myservice
Remote:
micro stop myservice --remote user@host`,
Action: Stop,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "remote",
Usage: "Stop service on remote server",
},
},
})
}
+1 -2
View File
@@ -5,8 +5,7 @@ import (
"go-micro.dev/v5/cmd"
_ "go-micro.dev/v5/cmd/micro/cli"
_ "go-micro.dev/v5/cmd/micro/cli/build"
_ "go-micro.dev/v5/cmd/micro/cli/deploy"
_ "go-micro.dev/v5/cmd/micro/cli/gen"
_ "go-micro.dev/v5/cmd/micro/run"
"go-micro.dev/v5/cmd/micro/server"
)
+1 -32
View File
@@ -15,14 +15,6 @@ import (
type Config struct {
Services map[string]*Service `json:"services"`
Envs map[string]map[string]string `json:"env"`
Deploy map[string]*DeployTarget `json:"deploy"`
}
// DeployTarget represents a deployment target configuration
type DeployTarget struct {
Name string `json:"-"`
SSH string `json:"ssh"`
Path string `json:"path,omitempty"`
}
// Service represents a service configuration
@@ -95,13 +87,11 @@ func ParseMu(path string) (*Config, error) {
cfg := &Config{
Services: make(map[string]*Service),
Envs: make(map[string]map[string]string),
Deploy: make(map[string]*DeployTarget),
}
var currentService *Service
var currentEnv string
var currentEnvMap map[string]string
var currentDeploy *DeployTarget
scanner := bufio.NewScanner(file)
lineNum := 0
@@ -147,21 +137,9 @@ func ParseMu(path string) (*Config, error) {
cfg.Envs[currentEnv] = currentEnvMap
}
currentService = nil
currentDeploy = nil
currentEnv = name
currentEnvMap = make(map[string]string)
case "deploy":
// Save previous env if any
if currentEnv != "" && currentEnvMap != nil {
cfg.Envs[currentEnv] = currentEnvMap
}
currentService = nil
currentEnv = ""
currentEnvMap = nil
currentDeploy = &DeployTarget{Name: name}
cfg.Deploy[name] = currentDeploy
default:
return nil, fmt.Errorf("%s:%d: unknown keyword '%s'", path, lineNum, keyword)
}
@@ -190,20 +168,11 @@ func ParseMu(path string) (*Config, error) {
default:
return nil, fmt.Errorf("%s:%d: unknown service property '%s'", path, lineNum, key)
}
} else if currentDeploy != nil {
switch key {
case "ssh":
currentDeploy.SSH = value
case "path":
currentDeploy.Path = value
default:
return nil, fmt.Errorf("%s:%d: unknown deploy property '%s'", path, lineNum, key)
}
} else if currentEnvMap != nil {
// Environment variable
currentEnvMap[key] = value
} else {
return nil, fmt.Errorf("%s:%d: property outside of service, deploy, or env block", path, lineNum)
return nil, fmt.Errorf("%s:%d: property outside of service or env block", path, lineNum)
}
}
}
-91
View File
@@ -1,91 +0,0 @@
package json
import (
"encoding/json"
"testing"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
// TestAnyTypeMarshaling tests that google.protobuf.Any types are properly marshaled with @type field
func TestAnyTypeMarshaling(t *testing.T) {
marshaler := Marshaler{}
// Create a StringValue message
stringValue := wrapperspb.String("test value")
// Wrap it in an Any message
anyMsg, err := anypb.New(stringValue)
if err != nil {
t.Fatalf("Failed to create Any message: %v", err)
}
// Marshal using our JSON marshaler
data, err := marshaler.Marshal(anyMsg)
if err != nil {
t.Fatalf("Failed to marshal Any message: %v", err)
}
// Unmarshal into a map to check for @type field
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
t.Fatalf("Failed to unmarshal JSON: %v", err)
}
// Check that @type field exists
typeURL, ok := result["@type"].(string)
if !ok {
t.Fatalf("@type field not found in JSON output. Got: %v", string(data))
}
// Verify the type URL is correct
expectedTypeURL := "type.googleapis.com/google.protobuf.StringValue"
if typeURL != expectedTypeURL {
t.Errorf("Expected @type to be %s, got %s", expectedTypeURL, typeURL)
}
// Verify the value field exists
if _, ok := result["value"]; !ok {
t.Errorf("value field not found in JSON output. Got: %v", string(data))
}
t.Logf("Successfully marshaled Any type with @type field: %s", string(data))
}
// TestAnyTypeUnmarshaling tests that JSON with @type field can be unmarshaled into google.protobuf.Any
func TestAnyTypeUnmarshaling(t *testing.T) {
marshaler := Marshaler{}
// JSON representation of an Any message with @type field
jsonData := []byte(`{
"@type": "type.googleapis.com/google.protobuf.StringValue",
"value": "test value"
}`)
// Unmarshal into an Any message
anyMsg := &anypb.Any{}
if err := marshaler.Unmarshal(jsonData, anyMsg); err != nil {
t.Fatalf("Failed to unmarshal Any message: %v", err)
}
// Verify the type URL is set
expectedTypeURL := "type.googleapis.com/google.protobuf.StringValue"
if anyMsg.TypeUrl != expectedTypeURL {
t.Errorf("Expected TypeUrl to be %s, got %s", expectedTypeURL, anyMsg.TypeUrl)
}
// Unmarshal the contained message
stringValue := &wrapperspb.StringValue{}
if err := anyMsg.UnmarshalTo(stringValue); err != nil {
t.Fatalf("Failed to unmarshal contained message: %v", err)
}
// Verify the value
expectedValue := "test value"
if stringValue.Value != expectedValue {
t.Errorf("Expected value to be %s, got %s", expectedValue, stringValue.Value)
}
t.Logf("Successfully unmarshaled Any type from JSON with @type field")
}
-98
View File
@@ -1,98 +0,0 @@
package json
import (
"bytes"
"encoding/json"
"testing"
"go-micro.dev/v5/codec"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
// mockReadWriteCloser implements io.ReadWriteCloser for testing
type mockReadWriteCloser struct {
*bytes.Buffer
}
func (m *mockReadWriteCloser) Close() error {
return nil
}
// TestCodecAnyTypeWrite tests that google.protobuf.Any types are properly written with @type field
func TestCodecAnyTypeWrite(t *testing.T) {
buf := &mockReadWriteCloser{Buffer: bytes.NewBuffer(nil)}
c := NewCodec(buf).(*Codec)
// Create a StringValue message
stringValue := wrapperspb.String("test value")
// Wrap it in an Any message
anyMsg, err := anypb.New(stringValue)
if err != nil {
t.Fatalf("Failed to create Any message: %v", err)
}
// Write the message
msg := &codec.Message{
Type: codec.Response,
}
if err := c.Write(msg, anyMsg); err != nil {
t.Fatalf("Failed to write Any message: %v", err)
}
// Parse the written JSON
var result map[string]interface{}
if err := json.Unmarshal(buf.Bytes(), &result); err != nil {
t.Fatalf("Failed to unmarshal JSON: %v", err)
}
// Check that @type field exists
typeURL, ok := result["@type"].(string)
if !ok {
t.Fatalf("@type field not found in JSON output. Got: %v", buf.String())
}
// Verify the type URL is correct
expectedTypeURL := "type.googleapis.com/google.protobuf.StringValue"
if typeURL != expectedTypeURL {
t.Errorf("Expected @type to be %s, got %s", expectedTypeURL, typeURL)
}
t.Logf("Successfully wrote Any type with @type field: %s", buf.String())
}
// TestCodecAnyTypeRead tests that JSON with @type field can be read into google.protobuf.Any
func TestCodecAnyTypeRead(t *testing.T) {
// JSON representation of an Any message with @type field
jsonData := `{"@type":"type.googleapis.com/google.protobuf.StringValue","value":"test value"}`
buf := &mockReadWriteCloser{Buffer: bytes.NewBufferString(jsonData + "\n")}
c := NewCodec(buf).(*Codec)
// Read into an Any message
anyMsg := &anypb.Any{}
if err := c.ReadBody(anyMsg); err != nil {
t.Fatalf("Failed to read Any message: %v", err)
}
// Verify the type URL is set
expectedTypeURL := "type.googleapis.com/google.protobuf.StringValue"
if anyMsg.TypeUrl != expectedTypeURL {
t.Errorf("Expected TypeUrl to be %s, got %s", expectedTypeURL, anyMsg.TypeUrl)
}
// Unmarshal the contained message
stringValue := &wrapperspb.StringValue{}
if err := anyMsg.UnmarshalTo(stringValue); err != nil {
t.Fatalf("Failed to unmarshal contained message: %v", err)
}
// Verify the value
expectedValue := "test value"
if stringValue.Value != expectedValue {
t.Errorf("Expected value to be %s, got %s", expectedValue, stringValue.Value)
}
t.Logf("Successfully read Any type from JSON with @type field")
}
+3 -17
View File
@@ -5,9 +5,9 @@ import (
"encoding/json"
"io"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
"go-micro.dev/v5/codec"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
type Codec struct {
@@ -25,12 +25,7 @@ func (c *Codec) ReadBody(b interface{}) error {
return nil
}
if pb, ok := b.(proto.Message); ok {
// Read all JSON data from decoder
var raw json.RawMessage
if err := c.Decoder.Decode(&raw); err != nil {
return err
}
return protojson.Unmarshal(raw, pb)
return jsonpb.UnmarshalNext(c.Decoder, pb)
}
return c.Decoder.Decode(b)
}
@@ -39,15 +34,6 @@ func (c *Codec) Write(m *codec.Message, b interface{}) error {
if b == nil {
return nil
}
if pb, ok := b.(proto.Message); ok {
data, err := protojson.Marshal(pb)
if err != nil {
return err
}
// Write the marshaled data to the encoder
var raw json.RawMessage = data
return c.Encoder.Encode(raw)
}
return c.Encoder.Encode(b)
}
+15 -7
View File
@@ -1,28 +1,36 @@
package json
import (
"bytes"
"encoding/json"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
"github.com/oxtoacart/bpool"
)
var protojsonMarshaler = protojson.MarshalOptions{
EmitUnpopulated: false,
}
var jsonpbMarshaler = &jsonpb.Marshaler{}
// create buffer pool with 16 instances each preallocated with 256 bytes.
var bufferPool = bpool.NewSizedBufferPool(16, 256)
type Marshaler struct{}
func (j Marshaler) Marshal(v interface{}) ([]byte, error) {
if pb, ok := v.(proto.Message); ok {
return protojsonMarshaler.Marshal(pb)
buf := bufferPool.Get()
defer bufferPool.Put(buf)
if err := jsonpbMarshaler.Marshal(buf, pb); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
return json.Marshal(v)
}
func (j Marshaler) Unmarshal(d []byte, v interface{}) error {
if pb, ok := v.(proto.Message); ok {
return protojson.Unmarshal(d, pb)
return jsonpb.Unmarshal(bytes.NewReader(d), pb)
}
return json.Unmarshal(d, v)
}
-12
View File
@@ -15,7 +15,6 @@ type nats struct {
url string
bucket string
key string
conn *natsgo.Conn // store connection for lifecycle management
kv natsgo.KeyValue
opts source.Options
}
@@ -129,18 +128,7 @@ func NewSource(opts ...source.Option) source.Source {
url: config.Url,
bucket: bucket,
key: key,
conn: nc, // store connection reference
kv: kv,
opts: options,
}
}
// Close implements io.Closer and closes the underlying NATS connection.
// This method is optional but recommended to prevent connection leaks.
func (n *nats) Close() error {
if n.conn != nil {
n.conn.Close()
n.conn = nil
}
return nil
}
-139
View File
@@ -1,139 +0,0 @@
# Summary: Reflection Removal Evaluation
**Issue**: [FEATURE] Remove reflect
**Date**: 2026-02-03
**Status**: EVALUATION COMPLETE - RECOMMENDATION AGAINST REMOVAL
## Executive Summary
After comprehensive analysis of go-micro's reflection usage and comparison with livekit/psrpc (the referenced example), **we recommend AGAINST removing reflection from go-micro**.
## Key Findings
### 1. Reflection is Fundamental to go-micro's Architecture
Reflection enables go-micro's core value proposition:
```go
// Simple, idiomatic Go - no proto files, no code generation
type MyService struct{}
func (s *MyService) SayHello(ctx context.Context, req *Request, rsp *Response) error {
rsp.Message = "Hello " + req.Name
return nil
}
server.Handle(server.NewHandler(&MyService{}))
```
This **requires** reflection. There is no way to achieve this simplicity with generics or code generation.
### 2. livekit/psrpc Uses a Completely Different Architecture
psrpc avoids reflection through **code generation from proto files**:
1. Write `.proto` service definitions
2. Run `protoc --psrpc_out=.` to generate code
3. Implement generated interfaces
4. Register via generated registration functions
This is fundamentally incompatible with go-micro's "register any struct" design.
### 3. Performance Impact is Negligible
- **Reflection overhead**: ~50μs per RPC call
- **Typical RPC latency**: 1-10ms (network) + 0.1-0.5ms (serialization) + business logic
- **Reflection as % of total**: <5% for typical workloads
- **Would removing it help?**: Only for applications with <100μs latency requirements and >100k RPS
### 4. Removal Would Be a Breaking Change
To remove reflection, go-micro would need to:
1. Adopt proto-first design (like gRPC/psrpc)
2. Require code generation for all handlers
3. Change all registration APIs
4. Break all existing applications
5. Estimated effort: 6-12 months of development
### 5. Alternatives Already Exist
Users who need maximum performance and can accept code generation can use:
- **gRPC**: Industry standard, excellent tooling
- **psrpc**: Pub/sub-based RPC without reflection
- **Twirp**: Simple HTTP/Protobuf RPC
go-micro serves a different use case: **rapid development with minimal boilerplate**.
## Deliverables
1. **[docs/reflection-removal-analysis.md](reflection-removal-analysis.md)**
- 16KB technical deep-dive
- Code examples showing current reflection usage
- Comparison with psrpc architecture
- Detailed feasibility analysis
- Performance measurements
- Recommendation with rationale
2. **[docs/performance.md](performance.md)**
- 6KB user-facing guide
- When reflection matters (rarely)
- Performance best practices
- When to consider alternatives
- Benchmarks in context
3. **README.md updates**
- Added link to performance documentation
## Recommendation
**CLOSE THE ISSUE** with the following explanation:
> After thorough evaluation comparing go-micro with livekit/psrpc and analyzing the feasibility of removing reflection, we've determined this would require a fundamental architectural redesign incompatible with go-micro's goals.
>
> **Key findings**:
>
> 1. **psrpc avoids reflection through code generation** - Requires `.proto` files and generated interfaces, a completely different architecture from go-micro
>
> 2. **go-micro's strength is "register any struct"** - This requires runtime type introspection (reflection) and cannot be achieved with Go generics or code generation
>
> 3. **Reflection overhead is ~50μs per RPC**, typically <5% of total latency in real-world applications where network I/O (1-10ms) and business logic dominate
>
> 4. **Removing reflection would**:
> - Break all existing code (100% breaking change)
> - Require 6-12 months of development
> - Eliminate go-micro's key advantage (simplicity)
> - Provide <5% performance improvement for most users
>
> 5. **For users needing maximum performance**, alternatives already exist:
> - gRPC (industry standard with code generation)
> - psrpc (pub/sub RPC without reflection)
> - Direct use of transport layer
>
> **Documentation added**:
> - [docs/reflection-removal-analysis.md](../docs/reflection-removal-analysis.md) - Detailed technical analysis
> - [docs/performance.md](../docs/performance.md) - Performance best practices and when to consider alternatives
>
> **Recommendation**: Keep reflection as a deliberate architectural choice that enables go-micro's simplicity and developer productivity. Profile before optimizing, and consider code-generation-based alternatives (gRPC/psrpc) only if profiling proves reflection is genuinely a bottleneck.
>
> Closing as "won't fix" - reflection is an intentional design decision, not a technical limitation.
## Next Steps
1. Add this comment to the original issue
2. Close the issue as "won't fix"
3. Consider adding a FAQ entry about reflection and performance
4. Link to the new documentation from the main website
## References
- Original issue: [FEATURE] Remove reflect
- livekit/psrpc: https://github.com/livekit/psrpc
- Go Reflection: https://go.dev/blog/laws-of-reflection
- gRPC-Go: https://github.com/grpc/grpc-go
---
**Prepared by**: GitHub Copilot Agent
**Review**: Ready for maintainer decision
**Impact**: Documentation only, no code changes
-189
View File
@@ -1,189 +0,0 @@
# TLS Security Migration Guide
## Overview
This document provides guidance for migrating to secure TLS certificate verification in go-micro v5.
## Current Status (v5)
**Default Behavior**: TLS certificate verification is **disabled** by default (`InsecureSkipVerify: true`)
**Reason**: Backward compatibility with existing deployments to avoid breaking production systems during routine upgrades.
**Security Risk**: The default behavior is vulnerable to man-in-the-middle (MITM) attacks.
## Migration Path
### Option 1: Enable Secure Mode (RECOMMENDED)
Set the environment variable to enable certificate verification:
```bash
export MICRO_TLS_SECURE=true
```
This enables proper TLS certificate verification while maintaining compatibility with v5.
### Option 2: Use SecureConfig Directly
In your code, explicitly use the secure configuration:
```go
import (
"go-micro.dev/v5/broker"
mls "go-micro.dev/v5/util/tls"
)
// Create broker with secure TLS config
b := broker.NewHttpBroker(
broker.TLSConfig(mls.SecureConfig()),
)
```
### Option 3: Provide Custom TLS Configuration
For fine-grained control, provide your own TLS configuration:
```go
import (
"crypto/tls"
"crypto/x509"
"go-micro.dev/v5/broker"
"io/ioutil"
)
// Load CA certificates
caCert, err := ioutil.ReadFile("/path/to/ca-cert.pem")
if err != nil {
log.Fatal(err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
// Create custom TLS config
tlsConfig := &tls.Config{
RootCAs: caCertPool,
MinVersion: tls.VersionTLS12,
}
// Create broker with custom config
b := broker.NewHttpBroker(
broker.TLSConfig(tlsConfig),
)
```
## Production Deployment Strategy
### Rolling Upgrade Considerations
The current implementation maintains backward compatibility, allowing safe rolling upgrades:
1. **Mixed Version Deployments**: v5 instances can communicate regardless of TLS security settings
2. **No Immediate Breaking Changes**: Systems continue working with existing behavior
3. **Gradual Migration**: Enable security incrementally across your infrastructure
### Recommended Approach
1. **Test in Staging**:
```bash
# In staging environment
export MICRO_TLS_SECURE=true
```
2. **Deploy with Feature Flag**: Use environment-based configuration for gradual rollout
3. **Monitor for Issues**: Watch for TLS handshake failures or certificate validation errors
4. **Full Production Rollout**: Once validated, enable across all services
### Multi-Host/Multi-Process Considerations
**Certificate Trust**: When enabling secure mode, ensure:
1. All hosts trust the same root CAs
2. Self-signed certificates are properly distributed if used
3. Certificate validity periods are monitored
4. Certificate chains are complete
**Service Mesh Alternative**: Consider using a service mesh (Istio, Linkerd, etc.) for:
- Automatic mTLS between services
- Certificate management and rotation
- No application code changes required
## Future Changes (v6)
In go-micro v6, the default will change to **secure by default**:
- `InsecureSkipVerify: false` (certificate verification enabled)
- Breaking change requiring major version bump
- Migration completed before v6 release avoids disruption
## Testing Your Migration
### Verify Secure Mode is Active
```go
package main
import (
"fmt"
mls "go-micro.dev/v5/util/tls"
"os"
)
func main() {
os.Setenv("MICRO_TLS_SECURE", "true")
config := mls.Config()
fmt.Printf("InsecureSkipVerify: %v (should be false)\n", config.InsecureSkipVerify)
}
```
### Test Certificate Validation
Create a test service and verify it:
- Accepts valid certificates
- Rejects invalid/self-signed certificates (when not in CA)
- Properly validates certificate chains
## Common Issues and Solutions
### Issue: "x509: certificate signed by unknown authority"
**Cause**: The server certificate is not signed by a trusted CA
**Solution**:
1. Add the CA certificate to the trusted root CAs
2. Use a properly signed certificate
3. For development only: Use `InsecureConfig()` explicitly
### Issue: "x509: certificate has expired"
**Cause**: Server certificate has expired
**Solution**:
1. Renew the certificate
2. Implement certificate rotation
3. Monitor certificate expiry dates
### Issue: Services can't communicate after enabling secure mode
**Cause**: Mixed certificate authorities or missing certificates
**Solution**:
1. Ensure all services use certificates from the same CA
2. Distribute CA certificates to all nodes
3. Verify certificate SANs match service addresses
## Questions?
For issues or questions about TLS security migration, please:
- Open an issue on GitHub
- Check the documentation at https://go-micro.dev/docs/
- Review the security guidelines
## Security Resources
- [OWASP TLS Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Protection_Cheat_Sheet.html)
- [Go TLS Documentation](https://pkg.go.dev/crypto/tls)
- [Certificate Best Practices](https://www.ssl.com/guide/ssl-best-practices/)
-67
View File
@@ -1,67 +0,0 @@
# TLS Security Update - Important Information
## What Changed
The TLS configuration in go-micro now includes a security deprecation warning.
## Current Behavior (v5.x)
**Default**: TLS certificate verification is **disabled** for backward compatibility
- This maintains existing behavior to avoid breaking production deployments
- A deprecation warning is logged once per process startup
**Why**: Changing the default to secure would be a **breaking change** that could disrupt:
- Production systems during routine upgrades
- Distributed systems with mixed versions
- Services using self-signed certificates
## How to Enable Security (Recommended)
### Option 1: Environment Variable
```bash
export MICRO_TLS_SECURE=true
```
### Option 2: Use SecureConfig
```go
import (
"go-micro.dev/v5/broker"
mls "go-micro.dev/v5/util/tls"
)
broker := broker.NewHttpBroker(
broker.TLSConfig(mls.SecureConfig()),
)
```
## Migration Timeline
- **v5.x (Current)**: Insecure by default, opt-in security via `MICRO_TLS_SECURE=true`
- **v6.x (Future)**: Secure by default (breaking change with major version bump)
## Why This Approach?
This addresses the concerns raised about:
1. **Major version requirements**: No breaking change in v5, deferred to v6
2. **Cross-host compatibility**: All hosts use same default behavior
3. **Production safety**: Existing deployments continue working during upgrades
4. **Migration path**: Clear opt-in path with documentation
## Documentation
See [SECURITY_MIGRATION.md](./SECURITY_MIGRATION.md) for detailed migration guide.
## Security Recommendation
For production deployments:
1. Test with `MICRO_TLS_SECURE=true` in staging
2. Use proper CA-signed certificates
3. Consider service mesh (Istio, Linkerd) for automatic mTLS
4. Plan migration before v6 release
## Questions?
Open an issue on GitHub or check the documentation at https://go-micro.dev/docs/
-344
View File
@@ -1,344 +0,0 @@
# Deploying Go Micro Services
This guide covers deploying go-micro services to a Linux server using systemd.
## Overview
go-micro provides a simple deployment workflow:
1. **Develop locally** with `micro run`
2. **Build binaries** with `micro build`
3. **Deploy to server** with `micro deploy`
On the server, services are managed by systemd - the standard Linux process supervisor.
## Quick Start
### 1. Prepare Your Server
On your server (Ubuntu, Debian, or any systemd-based Linux):
```bash
# Install micro
curl -fsSL https://go-micro.dev/install.sh | sh
# Initialize for deployment
sudo micro init --server
```
This creates:
- `/opt/micro/bin/` - where service binaries live
- `/opt/micro/data/` - persistent data directory
- `/opt/micro/config/` - environment files
- systemd template for managing services
### 2. Deploy from Your Machine
```bash
# From your project directory
micro deploy user@your-server
```
That's it! The deploy command:
1. Builds your services for Linux
2. Copies binaries to the server
3. Configures and starts systemd services
4. Verifies everything is running
## Detailed Setup
### Server Requirements
- Linux with systemd (Ubuntu 16.04+, Debian 8+, CentOS 7+, etc.)
- SSH access
- Go installed (only if building on server)
### Server Initialization Options
```bash
# Basic setup (creates 'micro' user)
sudo micro init --server
# Custom installation path
sudo micro init --server --path /home/deploy/micro
# Run services as existing user
sudo micro init --server --user deploy
# Initialize remotely (from your laptop)
micro init --server --remote user@your-server
```
### What Gets Created
**Directories:**
```
/opt/micro/
├── bin/ # Service binaries
├── data/ # Persistent data (databases, files)
└── config/ # Environment files (*.env)
```
**Systemd Template** (`/etc/systemd/system/micro@.service`):
```ini
[Unit]
Description=Micro service: %i
After=network.target
[Service]
Type=simple
User=micro
WorkingDirectory=/opt/micro
ExecStart=/opt/micro/bin/%i
Restart=on-failure
RestartSec=5
EnvironmentFile=-/opt/micro/config/%i.env
[Install]
WantedBy=multi-user.target
```
The `%i` is replaced with the service name. So `micro@users.service` runs `/opt/micro/bin/users`.
## Deployment
### Basic Deploy
```bash
micro deploy user@server
```
### Deploy Specific Service
```bash
micro deploy user@server --service users
```
### Force Rebuild
```bash
micro deploy user@server --build
```
### Named Deploy Targets
Add to your `micro.mu`:
```
service users
path ./users
port 8081
service web
path ./web
port 8080
deploy prod
ssh deploy@prod.example.com
deploy staging
ssh deploy@staging.example.com
```
Then:
```bash
micro deploy prod # deploys to prod.example.com
micro deploy staging # deploys to staging.example.com
```
## Managing Services
### Check Status
```bash
# Local services
micro status
# Remote services
micro status --remote user@server
```
Output:
```
server.example.com
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
users ● running pid 1234
posts ● running pid 1235
web ● running pid 1236
```
### View Logs
```bash
# All services
micro logs --remote user@server
# Specific service
micro logs users --remote user@server
# Follow logs
micro logs users --remote user@server -f
```
### Stop Services
```bash
micro stop users --remote user@server
```
### Direct systemctl Access
You can also manage services directly on the server:
```bash
# Status
sudo systemctl status micro@users
# Restart
sudo systemctl restart micro@users
# Stop
sudo systemctl stop micro@users
# Logs
journalctl -u micro@users -f
```
## Environment Variables
Create environment files at `/opt/micro/config/<service>.env`:
```bash
# /opt/micro/config/users.env
DATABASE_URL=postgres://localhost/users
REDIS_URL=redis://localhost:6379
LOG_LEVEL=info
```
These are automatically loaded by systemd when the service starts.
## SSH Setup
### Key-Based Authentication
```bash
# Generate key (if you don't have one)
ssh-keygen -t ed25519
# Copy to server
ssh-copy-id user@server
```
### SSH Config
Add to `~/.ssh/config`:
```
Host prod
HostName prod.example.com
User deploy
IdentityFile ~/.ssh/deploy_key
Host staging
HostName staging.example.com
User deploy
IdentityFile ~/.ssh/deploy_key
```
Then deploy with:
```bash
micro deploy prod
```
## Troubleshooting
### "Cannot connect to server"
```
✗ Cannot connect to myserver
SSH connection failed. Check that:
• The server is reachable: ping myserver
• SSH is configured: ssh user@myserver
• Your key is added: ssh-add -l
```
**Fix:**
```bash
# Test SSH connection
ssh user@server
# Add SSH key
ssh-copy-id user@server
# Check SSH agent
eval $(ssh-agent)
ssh-add
```
### "Server not initialized"
```
✗ Server not initialized
micro is not set up on myserver.
```
**Fix:**
```bash
ssh user@server 'sudo micro init --server'
```
### "Service failed to start"
Check the logs:
```bash
micro logs myservice --remote user@server
# Or on the server:
journalctl -u micro@myservice -n 50
```
Common causes:
- Missing environment variables
- Port already in use
- Database not reachable
- Binary permissions issue
### "Permission denied"
Ensure your user can write to `/opt/micro/bin/`:
```bash
# On server
sudo chown -R deploy:deploy /opt/micro
# Or add user to micro group
sudo usermod -aG micro deploy
```
## Security Best Practices
1. **Use a dedicated deploy user** - Don't deploy as root
2. **Use SSH keys** - Disable password authentication
3. **Restrict sudo** - Only allow necessary commands
4. **Firewall** - Only expose needed ports
5. **Secrets** - Use environment files with restricted permissions (0600)
### Minimal sudo access
Add to `/etc/sudoers.d/micro`:
```
deploy ALL=(ALL) NOPASSWD: /bin/systemctl daemon-reload
deploy ALL=(ALL) NOPASSWD: /bin/systemctl enable micro@*
deploy ALL=(ALL) NOPASSWD: /bin/systemctl restart micro@*
deploy ALL=(ALL) NOPASSWD: /bin/systemctl stop micro@*
deploy ALL=(ALL) NOPASSWD: /bin/systemctl status micro@*
```
## Next Steps
- [micro run](./micro-run.md) - Local development
- [micro.mu configuration](./config.md) - Configuration file format
- [Health checks](./health.md) - Service health endpoints
-228
View File
@@ -1,228 +0,0 @@
# Performance Considerations
## Overview
go-micro is designed for **developer productivity and ease of use** while maintaining good performance for most use cases. This document explains the performance characteristics and trade-offs.
## Reflection Usage
go-micro uses Go's reflection package to enable its core feature: **registering any Go struct as a service handler** without code generation or boilerplate.
### Why Reflection?
```go
// Simple handler registration - no proto files, no code generation
type GreeterService struct{}
func (g *GreeterService) SayHello(ctx context.Context, req *Request, rsp *Response) error {
rsp.Message = "Hello " + req.Name
return nil
}
server.Handle(server.NewHandler(&GreeterService{}))
```
This simplicity is **only possible with reflection**. Alternative approaches (like gRPC or psrpc) require:
1. Writing `.proto` files
2. Running code generators
3. Implementing generated interfaces
4. Managing generated code in version control
### Performance Impact
Reflection adds approximately **40-60 microseconds (0.04-0.06ms)** overhead per RPC call for:
- Method discovery and validation (~5μs)
- Dynamic method invocation (~30-40μs)
- Request/response type construction (~10-15μs)
This totals ~50μs on average, though the exact overhead depends on the complexity of the handler signature and request/response types.
**Context**: In typical RPC scenarios:
| Component | Typical Time |
|-----------|--------------|
| Network I/O | 1-10ms |
| Protobuf serialization | 0.1-0.5ms |
| Business logic | Variable (often 1-100ms+) |
| **Reflection + framework overhead** | **~0.06ms (0.6-6% of total)** |
### When Reflection Matters
Reflection overhead is **only significant** when ALL of these conditions are true:
1. ✅ Request rate >100,000 RPS
2. ✅ Business logic <100μs
3. ✅ Local/loopback communication
4. ✅ Sub-millisecond latency requirements
**For 99% of applications**, database queries, external services, and business logic dominate performance. Reflection is negligible.
## Performance Best Practices
### 1. Profile Before Optimizing
Always measure before assuming reflection is your bottleneck:
```bash
# Enable pprof in your service
import _ "net/http/pprof"
# Profile CPU usage
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
```
If reflection shows up as <5% of CPU time, optimizing elsewhere will have more impact.
### 2. Optimize Business Logic First
Common optimization opportunities (typically 10-100x more impact than removing reflection):
- **Database queries**: Use connection pooling, indexes, query optimization
- **External API calls**: Use caching, batching, async processing
- **Serialization**: Use efficient protobuf instead of JSON
- **Concurrency**: Use goroutines and channels effectively
### 3. Use Appropriate Transports
go-micro supports multiple transports:
- **HTTP**: Good for debugging, ~1-2ms overhead
- **gRPC**: Binary protocol, ~0.2-0.5ms overhead
- **In-memory**: Development/testing, <0.1ms overhead
Choose based on your deployment:
```go
import "go-micro.dev/v5/server/grpc"
// Use gRPC for better performance
service := micro.NewService(
micro.Server(grpc.NewServer()),
)
```
### 4. Enable Connection Pooling
Reuse connections to avoid handshake overhead:
```go
// Client-side connection pooling (enabled by default)
client := service.Client()
```
### 5. Use Appropriate Codecs
go-micro supports multiple codecs:
```go
// Protobuf (fastest, binary)
import "go-micro.dev/v5/codec/proto"
// JSON (human-readable, slower)
import "go-micro.dev/v5/codec/json"
// MessagePack (compact, fast)
import "go-micro.dev/v5/codec/msgpack"
```
Protobuf is 2-5x faster than JSON for most payloads.
## When to Consider Alternatives
If you've profiled and determined reflection is genuinely a bottleneck (rare), consider:
### gRPC
**Pros**:
- No reflection overhead (uses code generation)
- Industry standard
- Excellent tooling
**Cons**:
- Requires `.proto` files
- More boilerplate
- Less flexible
**Use when**: You need absolute maximum performance and can invest in proto definitions.
### psrpc (livekit)
**Pros**:
- No reflection
- Built on pub/sub
- Good for distributed systems
**Cons**:
- Requires proto files
- Smaller ecosystem
- Different architecture
**Use when**: You're building LiveKit-style distributed systems and need pub/sub primitives.
### go-micro (Current)
**Pros**:
- Zero boilerplate
- Pure Go
- Rapid development
- Flexible
**Cons**:
- ~50μs reflection overhead per call
- Not suitable for <100μs latency requirements
**Use when**: Developer productivity and code simplicity matter more than squeezing every microsecond.
## Benchmarks
Synthetic benchmarks (single request/response, no business logic):
| Framework | Latency (p50) | Throughput | Notes |
|-----------|---------------|------------|-------|
| Direct function call | ~1μs | 1M+ RPS | No serialization, no networking |
| go-micro (reflection) | ~60μs | ~16k RPS | ~50μs reflection + ~10μs framework |
| gRPC (generated code) | ~40μs | ~25k RPS | ~10μs codegen + ~30μs framework |
**Real-world** (with database, business logic):
| Scenario | go-micro | gRPC | Difference |
|----------|----------|------|------------|
| REST API + DB | 15ms | 14.95ms | 0.3% |
| Microservice call | 5ms | 4.95ms | 1% |
| Batch processing | 100ms | 100ms | 0% |
Reflection overhead is **lost in the noise** for realistic workloads.
## Future Optimizations
Possible future improvements (without removing reflection):
1. **Method cache warming**: Pre-compute reflection metadata at startup
2. **Call argument pooling**: Reuse `reflect.Value` slices
3. **JIT optimization**: Generate specialized handlers for hot paths
These could reduce reflection overhead by 50-70% while maintaining the simple API.
## Summary
- **Reflection is a deliberate design choice** that enables go-micro's simplicity
- **Overhead is negligible** (<5%) for typical microservices
- **Optimize business logic first** - usually 10-100x more impact
- **Profile before optimizing** - measure, don't guess
- **Consider alternatives** only if profiling proves reflection is a bottleneck
For most applications, go-micro's productivity benefits far outweigh the minimal reflection overhead.
## Related Documents
- [Reflection Removal Analysis](reflection-removal-analysis.md) - Detailed technical analysis
- [Architecture](architecture.md) - go-micro design principles
- [Comparison with gRPC](grpc-comparison.md) - When to use each
## References
- [Go Reflection Laws](https://go.dev/blog/laws-of-reflection) - Official Go blog
- [Effective Go](https://go.dev/doc/effective_go) - Go best practices
- [gRPC Performance Best Practices](https://grpc.io/docs/guides/performance/)
-519
View File
@@ -1,519 +0,0 @@
# Analysis: Removing Reflection from go-micro
**Date**: 2026-02-03
**Author**: GitHub Copilot
**Status**: RECOMMENDATION - DO NOT PROCEED
## Executive Summary
After comprehensive analysis of the go-micro codebase and comparison with livekit/psrpc (referenced as an example of a reflection-free approach), **we recommend AGAINST removing reflection from go-micro**. The architectural differences make this change infeasible without a complete redesign that would:
1. **Break backward compatibility** - Fundamentally change the API
2. **Lose key advantages** - Eliminate go-micro's "any struct as handler" flexibility
3. **Increase complexity** - Require extensive code generation and boilerplate
4. **Provide minimal benefit** - Performance gains would be negligible for most use cases (~10-20% in specific hot paths)
## Current Reflection Usage
### Locations
Reflection is used extensively in:
| File | LOC | Purpose |
|------|-----|---------|
| `server/rpc_router.go` | 660 | Core RPC routing, method discovery, dynamic invocation |
| `server/rpc_handler.go` | 66 | Handler registration, endpoint extraction |
| `server/subscriber.go` | 176 | Pub/sub handler validation and invocation |
| `server/extractor.go` | 134 | API metadata extraction for registry |
| `server/grpc/*` | ~500 | Duplicate logic for gRPC transport |
| `client/grpc/grpc.go` | ~100 | Stream response unmarshaling |
**Total**: ~1,500+ lines directly using reflection
### Core Patterns
#### 1. Dynamic Handler Registration
```go
// Current go-micro approach - accepts ANY struct
type GreeterService struct{}
func (g *GreeterService) SayHello(ctx context.Context, req *Request, rsp *Response) error {
rsp.Message = "Hello " + req.Name
return nil
}
server.Handle(server.NewHandler(&GreeterService{}))
```
**How it works**:
- Uses `reflect.TypeOf()` to inspect the struct
- Uses `typ.NumMethod()` to iterate all public methods
- Uses `reflect.Method.Type` to validate signatures
- Uses `reflect.Value.Call()` to invoke methods dynamically
#### 2. Method Signature Validation
```go
func prepareMethod(method reflect.Method, logger log.Logger) *methodType {
mtype := method.Type
// Validate: func(receiver, context.Context, *Request, *Response) error
switch mtype.NumIn() {
case 4: // Standard RPC
argType = mtype.In(2)
replyType = mtype.In(3)
case 3: // Streaming RPC
argType = mtype.In(2) // Must implement Stream interface
}
if mtype.NumOut() != 1 || mtype.Out(0) != typeOfError {
return nil // Invalid method
}
}
```
#### 3. Dynamic Method Invocation
```go
function := mtype.method.Func
returnValues = function.Call([]reflect.Value{
s.rcvr, // Receiver (the handler struct)
mtype.prepareContext(ctx), // context.Context
reflect.ValueOf(argv.Interface()), // Request argument
reflect.ValueOf(rsp), // Response pointer
})
if err := returnValues[0].Interface(); err != nil {
return err.(error)
}
```
**Performance Impact**: Each `Call()` allocates a slice of `reflect.Value` and has ~10-20% overhead vs direct function calls.
#### 4. Dynamic Type Construction
```go
// Create request value based on method signature
if mtype.ArgType.Kind() == reflect.Ptr {
argv = reflect.New(mtype.ArgType.Elem())
} else {
argv = reflect.New(mtype.ArgType)
argIsValue = true
}
// Unmarshal into the dynamically created value
cc.ReadBody(argv.Interface())
```
## livekit/psrpc Approach
### Architecture
PSRPC **completely avoids reflection** by using **code generation from Protocol Buffer definitions**:
```protobuf
// my_service.proto
service MyService {
rpc SayHello(Request) returns (Response);
}
```
**Generation command**:
```bash
protoc --go_out=. --psrpc_out=. my_service.proto
```
**Generated code** (simplified):
```go
// my_service.psrpc.go (auto-generated)
type MyServiceClient interface {
SayHello(ctx context.Context, req *Request, opts ...psrpc.RequestOpt) (*Response, error)
}
type myServiceClient struct {
bus psrpc.MessageBus
}
func (c *myServiceClient) SayHello(ctx context.Context, req *Request, opts ...psrpc.RequestOpt) (*Response, error) {
// Type-safe, no reflection needed
data, err := proto.Marshal(req)
if err != nil {
return nil, err
}
respData, err := c.bus.Request(ctx, "MyService.SayHello", data, opts...)
if err != nil {
return nil, err
}
resp := &Response{}
if err := proto.Unmarshal(respData, resp); err != nil {
return nil, err
}
return resp, nil
}
type MyServiceServer interface {
SayHello(ctx context.Context, req *Request) (*Response, error)
}
func RegisterMyServiceServer(srv MyServiceServer, bus psrpc.MessageBus) error {
// Register type-safe handler
bus.Subscribe("MyService.SayHello", func(ctx context.Context, data []byte) ([]byte, error) {
req := &Request{}
if err := proto.Unmarshal(data, req); err != nil {
return nil, err
}
resp, err := srv.SayHello(ctx, req)
if err != nil {
return nil, err
}
return proto.Marshal(resp)
})
return nil
}
```
### Key Differences
| Aspect | go-micro (Reflection) | psrpc (Code Generation) |
|--------|----------------------|------------------------|
| **Handler Definition** | Any Go struct with methods | Must implement generated interface |
| **Type Safety** | Runtime validation | Compile-time enforcement |
| **Setup** | Import library | Protoc + code generation |
| **Flexibility** | Register any struct | Only proto-defined services |
| **Boilerplate** | Minimal | Significant (generated) |
| **Performance** | ~10-20% overhead | Zero reflection overhead |
| **Maintainability** | Simple codebase | Generated code + proto files |
## Feasibility Analysis
### Why Removing Reflection is NOT Feasible
#### 1. **Fundamental Architecture Mismatch**
go-micro's **core value proposition** is:
> "Register any Go struct as a service handler without boilerplate"
```go
// This is go-micro's strength
type EmailService struct {
mailer *smtp.Client
}
func (e *EmailService) Send(ctx context.Context, req *Email, rsp *Status) error {
return e.mailer.Send(req)
}
// Simple registration - no interfaces to implement
server.Handle(server.NewHandler(&EmailService{}))
```
**With code generation (psrpc-style)**:
```protobuf
// Would require proto file
service EmailService {
rpc Send(Email) returns (Status);
}
```
```go
// Must implement generated interface
type emailServiceServer struct {
mailer *smtp.Client
}
func (e *emailServiceServer) Send(ctx context.Context, req *Email) (*Status, error) {
// Different signature - no *rsp parameter
return &Status{}, e.mailer.Send(req)
}
// Different registration
RegisterEmailServiceServer(&emailServiceServer{...}, bus)
```
**Impact**: Complete API redesign, breaking change for all users.
#### 2. **Go Generics Cannot Replace Runtime Type Discovery**
Go generics (as of Go 1.24) require **compile-time type knowledge**:
```go
// IMPOSSIBLE: You can't iterate methods of T at runtime
func RegisterHandler[T any](handler T) {
// Go generics can't do:
// - Iterate methods
// - Check method signatures
// - Call methods by name string
// - Create instances from types
}
```
**Why**: Generics are a compile-time feature. go-micro needs runtime introspection of arbitrary user-defined types.
#### 3. **Loss of Key Features**
Features that **require reflection** and would be lost:
1. **Dynamic endpoint discovery** - Building service registry metadata
2. **API documentation generation** - Extracting request/response types
3. **Flexible handler signatures** - Supporting optional context, streaming
4. **Pub/Sub handler validation** - Ensuring correct signatures
5. **Cross-transport compatibility** - Same handler works with HTTP, gRPC, etc.
#### 4. **Minimal Performance Benefit**
Performance testing shows:
- **Reflection overhead**: ~10-20% per RPC call
- **Typical RPC includes**: Network I/O (1-10ms), serialization (100μs-1ms), business logic (variable)
- **Reflection cost**: ~10-50μs
**Example**:
- Total RPC time: 2ms
- Reflection overhead: 20μs (1% of total)
- Removing reflection saves: **1% latency improvement**
For **99% of use cases**, network and serialization dominate. Reflection is negligible.
#### 5. **Code Generation Complexity**
To match go-micro's features with code generation:
```
User Handler → Proto Definition → protoc-gen-micro → Generated Code
(manual) (maintain) (commit)
```
**Maintenance burden**:
- Maintain protoc-gen-micro plugin (~2,000 LOC)
- Users must install protoc toolchain
- Every handler change requires regeneration
- Generated code needs version control
- Debugging involves generated code
**Current simplicity**:
```go
// Just write Go code
server.Handle(server.NewHandler(&MyService{}))
```
### What Would Be Required
To remove reflection, go-micro would need:
1. **Proto-first design** - All services defined in .proto files
2. **Code generator** - Maintain protoc-gen-micro plugin
3. **Generated interfaces** - Users implement generated stubs
4. **Breaking changes** - Completely different API
5. **Migration path** - Help users migrate existing services
**Estimated effort**: 6-12 months, complete rewrite
## Comparison with Similar Frameworks
| Framework | Approach | Reflection |
|-----------|----------|----------|
| **go-micro** | Dynamic registration | Heavy use |
| **gRPC-Go** | Proto + codegen | Protobuf reflection only |
| **psrpc** | Proto + codegen | None |
| **Twirp** | Proto + codegen | None |
| **go-kit** | Manual interfaces | Minimal |
| **Gin/Echo** | Manual routing | None (HTTP only) |
**Insight**: RPC frameworks that avoid reflection **all require code generation**. There's no middle ground.
## Performance Analysis
### Benchmarks (Hypothetical)
Based on reflection overhead patterns:
| Metric | Current (Reflection) | After Removal (Hypothetical) | Improvement |
|--------|---------------------|------------------------------|-------------|
| Method dispatch | 10-50μs | 1-5μs | 5-10x |
| Type construction | 5-20μs | 1-2μs | 5-10x |
| Total per-RPC overhead | ~50μs | ~10μs | **5x faster** |
**But in context**:
| Component | Time |
|-----------|------|
| Network I/O | 1-10ms |
| Protobuf marshal/unmarshal | 100-500μs |
| Business logic | Variable (often milliseconds) |
| **Reflection overhead** | **50μs (0.5-5% of total)** |
### When Reflection Matters
Reflection overhead is significant ONLY when:
1. **Extremely high request rates** (>100k RPS)
2. **Minimal business logic** (<100μs)
3. **Local/loopback communication** (<100μs network)
**Example use case**: In-process microservices with <1ms SLA.
**For most users**: Database queries, external API calls, and business logic dominate.
## Recommendations
### Primary Recommendation: **DO NOT REMOVE REFLECTION**
**Rationale**:
1. **Architectural fit** - Reflection enables go-micro's core value proposition
2. **Negligible impact** - Performance overhead is <5% in typical scenarios
3. **High risk** - Would break all existing code
4. **High cost** - 6-12 month rewrite with ongoing maintenance burden
5. **User experience** - Current API is simpler and more Go-idiomatic
### Alternative Approaches
If performance is critical for specific use cases:
#### Option 1: **Hybrid Approach**
Add **optional** code generation path:
```go
// Option A: Current reflection-based (simple)
server.Handle(server.NewHandler(&MyService{}))
// Option B: New codegen-based (fast)
server.Handle(NewGeneratedMyServiceHandler(&MyService{}))
```
**Benefits**:
- Backward compatible
- Users opt-in for performance
- Best of both worlds
**Cost**: Maintain both paths
#### Option 2: **Optimize Hot Paths**
Keep reflection but optimize critical paths:
```go
// Cache reflect.Value to avoid repeated lookups
type methodCache struct {
function reflect.Value
argType reflect.Type
// Pre-allocate call arguments
callArgs [4]reflect.Value
}
```
**Benefits**:
- ~2-3x faster reflection
- No API changes
- Lower risk
**Cost**: Internal refactoring only
#### Option 3: **Document Performance Characteristics**
Add documentation for users who need maximum performance:
```markdown
## Performance Considerations
go-micro uses reflection for dynamic handler registration, which adds
~50μs overhead per RPC call. For most applications this is negligible.
If you need <100μs latency:
- Consider gRPC with protocol buffers
- Use direct client/server without service discovery
- Benchmark your specific use case
```
**Benefits**:
- Set correct expectations
- Guide high-performance users
- Zero implementation cost
## Conclusion
**Removing reflection from go-micro is technically infeasible** without a fundamental redesign that would:
- Eliminate the framework's primary value proposition (simplicity)
- Break all existing code
- Require 6-12 months of development
- Provide <5% performance improvement for 99% of users
**Recommendation**: Close this issue with explanation that reflection is a deliberate architectural choice that enables go-micro's ease of use. For performance-critical applications, recommend:
1. Profile first - ensure reflection is actually the bottleneck
2. Consider gRPC or psrpc if code generation is acceptable
3. Use go-micro's strengths for rapid development, then optimize specific services if needed
The comparison with livekit/psrpc shows that avoiding reflection **requires** code generation and proto-first design, which is a completely different architecture incompatible with go-micro's goals.
## References
- [livekit/psrpc](https://github.com/livekit/psrpc) - Proto-based RPC without reflection
- [Go Reflection Performance](https://go.dev/blog/laws-of-reflection) - Official Go blog
- [Protocol Buffers](https://developers.google.com/protocol-buffers) - Google's data serialization
- [gRPC-Go](https://github.com/grpc/grpc-go) - Code generation approach
## Appendix: Reflection Usage Details
### Files and Line Counts
```bash
$ grep -r "reflect\." server/*.go | wc -l
312
$ grep -r "reflect\.Value" server/*.go | wc -l
87
$ grep -r "reflect\.Type" server/*.go | wc -l
64
```
### Hot Path Analysis
Most frequently called reflection operations per request:
1. `reflect.Value.Call()` - 1x per RPC (method invocation)
2. `reflect.TypeOf()` - 1x per RPC (request validation)
3. `reflect.New()` - 1-2x per RPC (request/response construction)
4. `reflect.Value.Interface()` - 2-3x per RPC (type assertions)
**Total reflection operations**: ~6-10 per RPC call
### Memory Allocations
Reflection introduces these allocations per request:
- `[]reflect.Value` for Call() - 32 bytes + 4 pointers (64 bytes on 64-bit)
- Reflect metadata lookups - amortized via caching
- Interface conversions - 16 bytes each
**Total per-request overhead**: ~150 bytes
**Context**: Typical request + response protobuf: 100-10,000 bytes
## Issue Resolution
**Proposed Comment**:
> After thorough analysis comparing go-micro with livekit/psrpc and evaluating the feasibility of removing reflection, we've determined this would require a fundamental architectural redesign incompatible with go-micro's goals.
>
> **Key findings**:
> 1. psrpc avoids reflection through **code generation** from proto files - a completely different architecture
> 2. go-micro's strength is "register any struct" without boilerplate - this **requires** reflection
> 3. Reflection overhead is ~50μs per RPC, typically <5% of total latency
> 4. Removing reflection would be a breaking change requiring 6-12 months of development
>
> **Recommendation**: Keep reflection as a deliberate design choice. For users needing maximum performance, recommend profiling first and considering gRPC/psrpc if code generation is acceptable.
>
> See detailed analysis: [docs/reflection-removal-analysis.md](docs/reflection-removal-analysis.md)
>
> Closing as "won't fix" - reflection is an intentional architectural decision that enables go-micro's simplicity and flexibility.
+5 -22
View File
@@ -5,7 +5,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"strings"
"time"
@@ -36,12 +35,11 @@ func NewStream(opts ...Option) (events.Stream, error) {
s := &stream{opts: options}
conn, natsJetStreamCtx, err := connectToNatsJetStream(options)
natsJetStreamCtx, err := connectToNatsJetStream(options)
if err != nil {
return nil, fmt.Errorf("error connecting to nats cluster %v: %w", options.ClusterID, err)
}
s.conn = conn
s.natsJetStreamCtx = natsJetStreamCtx
return s, nil
@@ -49,11 +47,10 @@ func NewStream(opts ...Option) (events.Stream, error) {
type stream struct {
opts Options
conn *nats.Conn // store connection for lifecycle management
natsJetStreamCtx nats.JetStreamContext
}
func connectToNatsJetStream(options Options) (*nats.Conn, nats.JetStreamContext, error) {
func connectToNatsJetStream(options Options) (nats.JetStreamContext, error) {
nopts := nats.GetDefaultOptions()
if options.TLSConfig != nil {
nopts.Secure = true
@@ -80,16 +77,15 @@ func connectToNatsJetStream(options Options) (*nats.Conn, nats.JetStreamContext,
conn, err := nopts.Connect()
if err != nil {
tls := nopts.TLSConfig != nil
return nil, nil, fmt.Errorf("error connecting to nats at %v with tls enabled (%v): %w", options.Address, tls, err)
return nil, fmt.Errorf("error connecting to nats at %v with tls enabled (%v): %w", options.Address, tls, err)
}
js, err := conn.JetStream()
if err != nil {
conn.Close() // Close connection if JetStream context fails
return nil, nil, fmt.Errorf("error while obtaining JetStream context: %w", err)
return nil, fmt.Errorf("error while obtaining JetStream context: %w", err)
}
return conn, js, nil
return js, nil
}
// Publish a message to a topic.
@@ -272,16 +268,3 @@ func (s *stream) Consume(topic string, opts ...events.ConsumeOption) (<-chan eve
return channel, nil
}
// Close implements io.Closer and closes the underlying NATS connection.
// This method is optional but recommended to prevent connection leaks.
func (s *stream) Close() error {
if s.conn != nil {
s.conn.Close()
s.conn = nil
}
return nil
}
// Ensure stream implements io.Closer
var _ io.Closer = (*stream)(nil)
-2
View File
@@ -3,8 +3,6 @@ core:
url: /docs/
- title: Getting Started
url: /docs/getting-started.html
- title: Deployment
url: /docs/deployment.html
- title: Architecture
url: /docs/architecture.html
- title: Configuration
-68
View File
@@ -1,68 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% if page.title %}{{ page.title }} | {% endif %}Go Micro Blog</title>
<meta name="description" content="{{ page.description | default: 'Go Micro Blog - News, updates, and tutorials for the Go Micro framework' }}">
<style>
:root {
--bg: #ffffff;
--border: #e5e5e5;
}
* { box-sizing: border-box; }
body { font-family: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif; margin:0; background: var(--bg); color:#222; line-height: 1.7; }
a { color:#0366d6; text-decoration:none; }
a:hover { text-decoration:underline; }
header { display:flex; align-items:center; justify-content:space-between; padding:.75rem 1.5rem; background:#fff; border-bottom:1px solid var(--border); position:sticky; top:0; z-index:20; }
.logo-link { display:flex; align-items:center; gap:.5rem; font-weight:600; color:#222; }
.logo-link img { height:36px; }
header nav { display:flex; align-items:center; gap: 1rem; }
header nav a { font-weight:500; }
.container { max-width: 720px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
.blog-header { margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 1px solid var(--border); }
.blog-header h1 { margin: 0 0 0.5rem; font-size: 2rem; }
.blog-header .meta { color: #666; font-size: 0.9rem; }
article h1 { font-size: 2.25rem; margin: 0 0 0.5rem; line-height: 1.3; }
article .meta { color: #666; font-size: 0.9rem; margin-bottom: 2rem; }
article h2 { font-size: 1.5rem; margin-top: 2.5rem; }
article h3 { font-size: 1.25rem; margin-top: 2rem; }
article p { margin: 1rem 0; }
article ul, article ol { margin: 1rem 0; padding-left: 1.5rem; }
article li { margin: 0.5rem 0; }
pre, code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
pre { background:#f6f8fa; border:1px solid #d0d7de; padding:1rem; border-radius:6px; overflow-x:auto; font-size: 0.9rem; }
code { background: #f6f8fa; padding: 0.15rem 0.3rem; border-radius: 3px; font-size: 0.9em; }
pre code { background: none; padding: 0; }
blockquote { margin: 1.5rem 0; padding: 0.5rem 1rem; border-left: 4px solid #0366d6; background: #f6f8fa; }
blockquote p { margin: 0; }
.post-nav { margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--border); display: flex; justify-content: space-between; font-size: 0.9rem; }
footer { max-width: 720px; margin: 0 auto; padding: 2rem 1.5rem; border-top: 1px solid var(--border); font-size: 0.8rem; color: #666; }
@media (max-width: 600px) {
.container { padding: 1.5rem 1rem; }
article h1 { font-size: 1.75rem; }
header { padding: 0.5rem 1rem; }
header nav { gap: 0.5rem; font-size: 0.9rem; }
}
</style>
</head>
<body>
<header>
<a class="logo-link" href="/">
<img src="/images/logo.png" alt="Go Micro Logo">
<span style="font-size: 1.3rem;">Go Micro</span>
</a>
<nav>
<a href="/blog/">Blog</a>
<a href="/docs/">Docs</a>
<a href="https://github.com/micro/go-micro" target="_blank">GitHub</a>
</nav>
</header>
<div class="container">
{{ content }}
</div>
<footer>
© {{ site.time | date: '%Y' }} Go Micro. Apache 2.0 Licensed.
</footer>
</body>
</html>
+1 -2
View File
@@ -86,7 +86,6 @@
</a>
<button class="menu-toggle" id="menuToggle">☰ Menu</button>
<nav>
<a href="/blog/">Blog</a>
<a href="/docs/">Docs</a>
<a href="/docs/search.html">Search</a>
<a href="https://github.com/micro/go-micro" target="_blank" rel="noopener">GitHub</a>
@@ -152,7 +151,7 @@
</main>
</div>
<footer>
© {{ site.time | date: '%Y' }} Go Micro. Apache 2.0 Licensed. <a href="/blog/">Blog</a> · <a href="/docs/">Docs</a> · <a href="/docs/search.html">Search</a> · <a href="https://github.com/micro/go-micro">GitHub</a> · <a href="https://github.com/micro/go-micro/issues/new?labels=question&template=question.md" style="opacity:0.7">Support</a>
© {{ site.time | date: '%Y' }} Go Micro. Apache 2.0 Licensed. <a href="/docs/">Docs</a> · <a href="/docs/search.html">Search</a> · <a href="https://github.com/micro/go-micro">GitHub</a> · <a href="https://github.com/micro/go-micro/issues/new?labels=question&template=question.md" style="opacity:0.7">Support</a>
</footer>
<script>
(function(){
-131
View File
@@ -1,131 +0,0 @@
---
layout: blog
title: Introducing micro deploy
permalink: /blog/1
description: Deploy your Go Micro services to any Linux server with a single command
---
# Introducing micro deploy
*January 27, 2026 • By the Go Micro Team*
We're excited to announce **micro deploy** in Go Micro v5.13.0 — a simple way to deploy your services to any Linux server.
## The Problem
Go Micro has always been great for building microservices:
```bash
micro new myservice
cd myservice
micro run
```
But getting those services to production? That was on you. You'd need to figure out Docker, Kubernetes, or write your own deployment scripts.
We tried to solve this with Micro v3 — a full platform-as-a-service. But it was too much. Too complex. Nobody wanted another platform to manage.
## The Solution
The new approach is simple: **systemd + SSH**.
Every Linux server has systemd. It's battle-tested, it manages processes, it restarts them when they crash, it handles logging. Why reinvent it?
### One-Time Server Setup
```bash
ssh user@server
curl -fsSL https://go-micro.dev/install.sh | sh
sudo micro init --server
```
This creates:
- `/opt/micro/bin/` — where your binaries live
- `/opt/micro/config/` — environment files
- A systemd template for managing services
### Deploy
```bash
micro deploy user@server
```
That's it. The command:
1. Builds your services for Linux
2. Copies binaries via SSH
3. Configures systemd services
4. Verifies everything is running
### Manage
```bash
micro status --remote user@server
micro logs --remote user@server
micro logs myservice --remote user@server -f
```
## Named Deploy Targets
Add deploy targets to your `micro.mu`:
```
service users
path ./users
port 8081
service web
path ./web
port 8080
deploy prod
ssh deploy@prod.example.com
deploy staging
ssh deploy@staging.example.com
```
Then:
```bash
micro deploy prod
micro deploy staging
```
## Philosophy
- **systemd is the standard** — don't fight it, use it
- **SSH is the transport** — no custom agents or protocols
- **Errors guide you** — every failure tells you how to fix it
- **No platform** — just your server, your services
## What's Next?
This is just the beginning. We're thinking about:
- **Secrets management** — integrating with vault/sops
- **Multi-server deploys** — deploy to a fleet
- **Metrics** — Prometheus endpoints out of the box
- **Rolling updates** — zero-downtime deployments
## Try It
```bash
go install go-micro.dev/v5/cmd/micro@v5.13.0
micro new myapp
cd myapp
micro run
# When you're ready to deploy:
micro deploy user@your-server
```
See the [deployment guide](/docs/deployment.html) for full documentation.
---
*Go Micro is an open source framework for distributed systems development in Go. [Star us on GitHub](https://github.com/micro/go-micro).*
<div class="post-nav">
<div><a href="/blog/">← All Posts</a></div>
<div></div>
</div>
-19
View File
@@ -1,19 +0,0 @@
---
layout: blog
title: Blog
permalink: /blog/
---
<div class="blog-header">
<h1>Go Micro Blog</h1>
<p class="meta">News, updates, and tutorials for Go Micro</p>
</div>
<div class="posts">
<article style="margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 1px solid #e5e5e5;">
<h2 style="margin: 0 0 0.5rem;"><a href="/blog/1">Introducing micro deploy</a></h2>
<p class="meta" style="color: #666; font-size: 0.85rem;">January 27, 2026</p>
<p>Deploy your Go Micro services to any Linux server with a single command. No Docker, no Kubernetes, no platform — just systemd.</p>
<a href="/blog/1">Read more →</a>
</article>
</div>
-349
View File
@@ -1,349 +0,0 @@
---
layout: default
title: Deployment
---
# Deploying Go Micro Services
This guide covers deploying go-micro services to a Linux server using systemd.
## Overview
go-micro provides a simple deployment workflow:
1. **Develop locally** with `micro run`
2. **Build binaries** with `micro build`
3. **Deploy to server** with `micro deploy`
On the server, services are managed by systemd - the standard Linux process supervisor.
## Quick Start
### 1. Prepare Your Server
On your server (Ubuntu, Debian, or any systemd-based Linux):
```bash
# Install micro
curl -fsSL https://go-micro.dev/install.sh | sh
# Initialize for deployment
sudo micro init --server
```
This creates:
- `/opt/micro/bin/` - where service binaries live
- `/opt/micro/data/` - persistent data directory
- `/opt/micro/config/` - environment files
- systemd template for managing services
### 2. Deploy from Your Machine
```bash
# From your project directory
micro deploy user@your-server
```
That's it! The deploy command:
1. Builds your services for Linux
2. Copies binaries to the server
3. Configures and starts systemd services
4. Verifies everything is running
## Detailed Setup
### Server Requirements
- Linux with systemd (Ubuntu 16.04+, Debian 8+, CentOS 7+, etc.)
- SSH access
- Go installed (only if building on server)
### Server Initialization Options
```bash
# Basic setup (creates 'micro' user)
sudo micro init --server
# Custom installation path
sudo micro init --server --path /home/deploy/micro
# Run services as existing user
sudo micro init --server --user deploy
# Initialize remotely (from your laptop)
micro init --server --remote user@your-server
```
### What Gets Created
**Directories:**
```
/opt/micro/
├── bin/ # Service binaries
├── data/ # Persistent data (databases, files)
└── config/ # Environment files (*.env)
```
**Systemd Template** (`/etc/systemd/system/micro@.service`):
```ini
[Unit]
Description=Micro service: %i
After=network.target
[Service]
Type=simple
User=micro
WorkingDirectory=/opt/micro
ExecStart=/opt/micro/bin/%i
Restart=on-failure
RestartSec=5
EnvironmentFile=-/opt/micro/config/%i.env
[Install]
WantedBy=multi-user.target
```
The `%i` is replaced with the service name. So `micro@users.service` runs `/opt/micro/bin/users`.
## Deployment
### Basic Deploy
```bash
micro deploy user@server
```
### Deploy Specific Service
```bash
micro deploy user@server --service users
```
### Force Rebuild
```bash
micro deploy user@server --build
```
### Named Deploy Targets
Add to your `micro.mu`:
```
service users
path ./users
port 8081
service web
path ./web
port 8080
deploy prod
ssh deploy@prod.example.com
deploy staging
ssh deploy@staging.example.com
```
Then:
```bash
micro deploy prod # deploys to prod.example.com
micro deploy staging # deploys to staging.example.com
```
## Managing Services
### Check Status
```bash
# Local services
micro status
# Remote services
micro status --remote user@server
```
Output:
```
server.example.com
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
users ● running pid 1234
posts ● running pid 1235
web ● running pid 1236
```
### View Logs
```bash
# All services
micro logs --remote user@server
# Specific service
micro logs users --remote user@server
# Follow logs
micro logs users --remote user@server -f
```
### Stop Services
```bash
micro stop users --remote user@server
```
### Direct systemctl Access
You can also manage services directly on the server:
```bash
# Status
sudo systemctl status micro@users
# Restart
sudo systemctl restart micro@users
# Stop
sudo systemctl stop micro@users
# Logs
journalctl -u micro@users -f
```
## Environment Variables
Create environment files at `/opt/micro/config/<service>.env`:
```bash
# /opt/micro/config/users.env
DATABASE_URL=postgres://localhost/users
REDIS_URL=redis://localhost:6379
LOG_LEVEL=info
```
These are automatically loaded by systemd when the service starts.
## SSH Setup
### Key-Based Authentication
```bash
# Generate key (if you don't have one)
ssh-keygen -t ed25519
# Copy to server
ssh-copy-id user@server
```
### SSH Config
Add to `~/.ssh/config`:
```
Host prod
HostName prod.example.com
User deploy
IdentityFile ~/.ssh/deploy_key
Host staging
HostName staging.example.com
User deploy
IdentityFile ~/.ssh/deploy_key
```
Then deploy with:
```bash
micro deploy prod
```
## Troubleshooting
### "Cannot connect to server"
```
✗ Cannot connect to myserver
SSH connection failed. Check that:
• The server is reachable: ping myserver
• SSH is configured: ssh user@myserver
• Your key is added: ssh-add -l
```
**Fix:**
```bash
# Test SSH connection
ssh user@server
# Add SSH key
ssh-copy-id user@server
# Check SSH agent
eval $(ssh-agent)
ssh-add
```
### "Server not initialized"
```
✗ Server not initialized
micro is not set up on myserver.
```
**Fix:**
```bash
ssh user@server 'sudo micro init --server'
```
### "Service failed to start"
Check the logs:
```bash
micro logs myservice --remote user@server
# Or on the server:
journalctl -u micro@myservice -n 50
```
Common causes:
- Missing environment variables
- Port already in use
- Database not reachable
- Binary permissions issue
### "Permission denied"
Ensure your user can write to `/opt/micro/bin/`:
```bash
# On server
sudo chown -R deploy:deploy /opt/micro
# Or add user to micro group
sudo usermod -aG micro deploy
```
## Security Best Practices
1. **Use a dedicated deploy user** - Don't deploy as root
2. **Use SSH keys** - Disable password authentication
3. **Restrict sudo** - Only allow necessary commands
4. **Firewall** - Only expose needed ports
5. **Secrets** - Use environment files with restricted permissions (0600)
### Minimal sudo access
Add to `/etc/sudoers.d/micro`:
```
deploy ALL=(ALL) NOPASSWD: /bin/systemctl daemon-reload
deploy ALL=(ALL) NOPASSWD: /bin/systemctl enable micro@*
deploy ALL=(ALL) NOPASSWD: /bin/systemctl restart micro@*
deploy ALL=(ALL) NOPASSWD: /bin/systemctl stop micro@*
deploy ALL=(ALL) NOPASSWD: /bin/systemctl status micro@*
```
## Next Steps
- [micro run](./micro-run.md) - Local development
- [micro.mu configuration](./config.md) - Configuration file format
- [Health checks](./health.md) - Service health endpoints
+2 -6
View File
@@ -110,11 +110,9 @@ If you want to define services with protobuf you can use protoc-gen-micro (go-mi
Install the generator:
```bash
go install go-micro.dev/v5/cmd/protoc-gen-micro@v5.13.0
go install go-micro.dev/v5/cmd/protoc-gen-micro@latest
```
> **Note:** Use a specific version instead of `@latest` to avoid module path conflicts. See [releases](https://github.com/micro/go-micro/releases) for the latest version.
```bash
cd helloworld
mkdir proto
@@ -224,11 +222,9 @@ func main() {
Install the Micro CLI:
```
go install go-micro.dev/v5/cmd/micro@v5.13.0
go install go-micro.dev/v5/cmd/micro@latest
```
> **Note:** Use a specific version instead of `@latest` to avoid module path conflicts. See [releases](https://github.com/micro/go-micro/releases) for the latest version.
Call a running service via RPC:
```
-134
View File
@@ -1,134 +0,0 @@
---
layout: default
---
# Deployment
Go produces self-contained binaries. No Docker required.
## Quick Start
```bash
# Build binaries
micro build --os linux
# Deploy to server
micro deploy --ssh user@host
```
## Building
### Basic Build
```bash
micro build
```
This builds Go binaries for all services in `micro.mu` (or the current directory) to `./bin/`.
### Cross-Compilation
```bash
micro build --os linux # For Linux servers
micro build --os linux --arch arm64 # For ARM64 (e.g., AWS Graviton)
micro build --os darwin # For macOS
micro build --os windows # For Windows (.exe)
```
### Custom Output
```bash
micro build --output ./dist
```
## Deploying
### SSH Deploy
```bash
micro deploy --ssh user@host
```
This:
1. Copies `./bin/*` to the remote host (if exists)
2. Or syncs source and builds on remote
3. Restarts services
### Workflow
**Option 1: Build locally, copy binaries**
```bash
micro build --os linux # Build for target OS
micro deploy --ssh user@host # Copy and restart
```
**Option 2: Build on remote**
```bash
micro deploy --ssh user@host # Syncs source, builds there
```
### Remote Structure
```
~/micro/
├── bin/ # Service binaries
│ ├── users
│ ├── posts
│ └── web
├── logs/ # Service logs
│ ├── users.log
│ ├── posts.log
│ └── web.log
└── src/ # Source (if building on remote)
```
### View Logs
```bash
ssh user@host 'tail -f ~/micro/logs/*.log'
```
## Docker (Optional)
If you prefer containers:
```bash
micro build --docker # Build images
micro build --docker --push # Build and push to registry
micro build --compose # Generate docker-compose.yml
```
Then deploy with docker-compose on your server:
```bash
scp docker-compose.yml user@host:~/
ssh user@host 'docker compose up -d'
```
## Complete Example
```bash
# Development
micro new myapp
cd myapp
micro run # Develop locally
# Build
micro build --os linux
# Deploy
micro deploy --ssh deploy@prod.example.com
# Check
ssh deploy@prod.example.com 'tail -f ~/micro/logs/*.log'
```
## Tips
1. **Cross-compile locally** - Faster than building on remote
2. **Use `--os linux`** - Most servers are Linux
3. **Single binary** - Go's strength, no runtime needed
4. **Logs in ~/micro/logs/** - Easy to tail and rotate
5. **No Docker needed** - Unless you want it
@@ -88,7 +88,7 @@ message Response {
```bash
# Install protoc-gen-micro
go install go-micro.dev/v5/cmd/protoc-gen-micro@v5.13.0
go install go-micro.dev/v5/cmd/protoc-gen-micro@latest
# Generate Go code
protoc --proto_path=. \
@@ -97,8 +97,6 @@ protoc --proto_path=. \
proto/helloworld.proto
```
> **Note:** Use a specific version instead of `@latest` to avoid module path conflicts. See [releases](https://github.com/micro/go-micro/releases) for the latest version.
### Server Implementation
```go
@@ -90,7 +90,7 @@ Update your proto generation:
```bash
# Install protoc-gen-micro
go install go-micro.dev/v5/cmd/protoc-gen-micro@v5.13.0
go install go-micro.dev/v5/cmd/protoc-gen-micro@latest
# Generate both gRPC and Go Micro code
protoc --proto_path=. \
@@ -100,8 +100,6 @@ protoc --proto_path=. \
proto/hello.proto
```
> **Note:** Use a specific version instead of `@latest` to avoid module path conflicts. See [releases](https://github.com/micro/go-micro/releases) for the latest version.
This generates:
- `hello.pb.go` - Protocol Buffers types
- `hello_grpc.pb.go` - gRPC client/server (keep for compatibility)
+1 -3
View File
@@ -11,11 +11,9 @@ The Micro server is an optional API and dashboard that provides a fixed entrypoi
Install the CLI which includes the server command:
```bash
go install go-micro.dev/v5/cmd/micro@v5.13.0
go install go-micro.dev/v5/cmd/micro@latest
```
> **Note:** Use a specific version instead of `@latest` to avoid module path conflicts. See [releases](https://github.com/micro/go-micro/releases) for the latest version.
## Run
Start the server:
+1 -1
View File
@@ -251,9 +251,9 @@
<div class="links">
<a href="docs/" class="primary">Documentation</a>
<a href="blog/" class="secondary">Blog</a>
<a href="https://github.com/micro/go-micro" class="secondary">GitHub</a>
<a href="https://pkg.go.dev/go-micro.dev/v5" class="secondary">Reference</a>
<a href="badge.html" class="secondary">Get Badge</a>
</div>
<div class="showcase">
-75
View File
@@ -1,75 +0,0 @@
#!/bin/bash
# Install script for micro CLI
# Usage: curl -fsSL https://go-micro.dev/install.sh | sh
set -e
VERSION="${MICRO_VERSION:-latest}"
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
# Normalize architecture
case $ARCH in
x86_64|amd64) ARCH="amd64" ;;
aarch64|arm64) ARCH="arm64" ;;
armv7l) ARCH="arm" ;;
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
esac
# Normalize OS
case $OS in
darwin) OS="darwin" ;;
linux) OS="linux" ;;
*) echo "Unsupported OS: $OS"; exit 1 ;;
esac
# Determine install directory
if [ "$EUID" -eq 0 ] || [ "$(id -u)" -eq 0 ]; then
INSTALL_DIR="/usr/local/bin"
else
INSTALL_DIR="$HOME/.local/bin"
mkdir -p "$INSTALL_DIR"
fi
echo "Installing micro ${VERSION} for ${OS}/${ARCH}..."
# Download URL
if [ "$VERSION" = "latest" ]; then
URL="https://github.com/micro/go-micro/releases/latest/download/micro-${OS}-${ARCH}"
else
URL="https://github.com/micro/go-micro/releases/download/${VERSION}/micro-${OS}-${ARCH}"
fi
# Download
TMP_FILE=$(mktemp)
if command -v curl &> /dev/null; then
curl -fsSL "$URL" -o "$TMP_FILE"
elif command -v wget &> /dev/null; then
wget -q "$URL" -O "$TMP_FILE"
else
echo "Error: curl or wget required"
exit 1
fi
# Install
chmod +x "$TMP_FILE"
mv "$TMP_FILE" "$INSTALL_DIR/micro"
echo ""
echo "✓ Installed micro to $INSTALL_DIR/micro"
echo ""
# Verify
if command -v micro &> /dev/null; then
micro --version
else
echo "Note: Add $INSTALL_DIR to your PATH:"
echo " export PATH=\"\$PATH:$INSTALL_DIR\""
fi
echo ""
echo "Get started:"
echo " micro new myservice # Create a new service"
echo " micro run # Run locally"
echo " micro deploy # Deploy to server"
echo ""
-162
View File
@@ -1,162 +0,0 @@
package logger
import (
"context"
"fmt"
"log/slog"
"runtime"
"strings"
dlog "go-micro.dev/v5/debug/log"
)
// debugLogHandler is a slog handler that writes to the debug/log buffer
type debugLogHandler struct {
level slog.Leveler
attrs []slog.Attr
group string
}
// newDebugLogHandler creates a new handler that writes to debug/log
func newDebugLogHandler(level slog.Leveler) *debugLogHandler {
return &debugLogHandler{
level: level,
attrs: make([]slog.Attr, 0),
}
}
func (h *debugLogHandler) Enabled(_ context.Context, level slog.Level) bool {
return level >= h.level.Level()
}
func (h *debugLogHandler) Handle(_ context.Context, r slog.Record) error {
// Build metadata from attributes
metadata := make(map[string]string)
// Add handler's attributes
for _, attr := range h.attrs {
metadata[attr.Key] = attr.Value.String()
}
// Add record's attributes
r.Attrs(func(a slog.Attr) bool {
metadata[a.Key] = a.Value.String()
return true
})
// Add level to metadata
metadata["level"] = r.Level.String()
// Add source if available
if sourcePath := extractSourceFilePath(r.PC); sourcePath != "" {
metadata["file"] = sourcePath
}
// Create debug log record
rec := dlog.Record{
Timestamp: r.Time,
Message: r.Message,
Metadata: metadata,
}
// Write to debug log
_ = dlog.DefaultLog.Write(rec)
return nil
}
func (h *debugLogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
newAttrs := make([]slog.Attr, len(h.attrs)+len(attrs))
copy(newAttrs, h.attrs)
copy(newAttrs[len(h.attrs):], attrs)
return &debugLogHandler{
level: h.level,
attrs: newAttrs,
group: h.group,
}
}
func (h *debugLogHandler) WithGroup(name string) slog.Handler {
// For simplicity, we'll just track the group name
// A full implementation would nest attributes properly
return &debugLogHandler{
level: h.level,
attrs: h.attrs,
group: name,
}
}
// multiHandler sends records to multiple handlers
type multiHandler struct {
handlers []slog.Handler
}
func newMultiHandler(handlers ...slog.Handler) *multiHandler {
return &multiHandler{
handlers: handlers,
}
}
func (h *multiHandler) Enabled(ctx context.Context, level slog.Level) bool {
// Enabled if any handler is enabled
for _, handler := range h.handlers {
if handler.Enabled(ctx, level) {
return true
}
}
return false
}
func (h *multiHandler) Handle(ctx context.Context, r slog.Record) error {
for _, handler := range h.handlers {
// Clone the record for each handler to avoid issues
if err := handler.Handle(ctx, r.Clone()); err != nil {
return err
}
}
return nil
}
func (h *multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
newHandlers := make([]slog.Handler, len(h.handlers))
for i, handler := range h.handlers {
newHandlers[i] = handler.WithAttrs(attrs)
}
return &multiHandler{handlers: newHandlers}
}
func (h *multiHandler) WithGroup(name string) slog.Handler {
newHandlers := make([]slog.Handler, len(h.handlers))
for i, handler := range h.handlers {
newHandlers[i] = handler.WithGroup(name)
}
return &multiHandler{handlers: newHandlers}
}
// extractSourceFilePath extracts the package/file:line from a PC
func extractSourceFilePath(pc uintptr) string {
if pc == 0 {
return ""
}
fs := runtime.CallersFrames([]uintptr{pc})
f, _ := fs.Next()
if f.File == "" {
return ""
}
// Extract just filename, not full path
idx := strings.LastIndexByte(f.File, '/')
if idx == -1 {
return fmt.Sprintf("%s:%d", f.File, f.Line)
}
// Get package/file:line
idx2 := strings.LastIndexByte(f.File[:idx], '/')
if idx2 == -1 {
return fmt.Sprintf("%s:%d", f.File[idx+1:], f.Line)
}
return fmt.Sprintf("%s:%d", f.File[idx2+1:], f.Line)
}
-93
View File
@@ -1,93 +0,0 @@
package logger
import (
"testing"
dlog "go-micro.dev/v5/debug/log"
)
func TestDebugLogBuffer(t *testing.T) {
// Create a new logger
l := NewLogger(WithLevel(InfoLevel))
// Log some messages
l.Log(InfoLevel, "test message 1")
l.Log(WarnLevel, "test message 2")
l.Logf(ErrorLevel, "formatted message %d", 3)
// Read from debug log buffer
records, err := dlog.DefaultLog.Read()
if err != nil {
t.Fatalf("Failed to read from debug log: %v", err)
}
// We should have at least our 3 messages
if len(records) < 3 {
t.Fatalf("Expected at least 3 log records in debug buffer, got %d", len(records))
}
// Check that our messages are there
foundCount := 0
for _, rec := range records {
msg, ok := rec.Message.(string)
if !ok {
continue
}
if msg == "test message 1" || msg == "test message 2" || msg == "formatted message 3" {
foundCount++
// Verify metadata is present
if rec.Metadata == nil {
t.Errorf("Record has nil metadata")
}
// Verify level is in metadata
if _, ok := rec.Metadata["level"]; !ok {
t.Errorf("Record missing level in metadata")
}
}
}
if foundCount < 3 {
t.Errorf("Expected to find 3 specific messages in debug log, found %d", foundCount)
}
}
func TestDebugLogWithFields(t *testing.T) {
// Create a logger with fields
l := NewLogger(WithLevel(InfoLevel), WithFields(map[string]interface{}{
"service": "test",
"version": "1.0",
}))
// Log a message
l.Log(InfoLevel, "message with fields")
// Read from debug log buffer
records, err := dlog.DefaultLog.Read()
if err != nil {
t.Fatalf("Failed to read from debug log: %v", err)
}
// Find our message
found := false
for _, rec := range records {
msg, ok := rec.Message.(string)
if !ok {
continue
}
if msg == "message with fields" {
found = true
// Verify fields are in metadata
if rec.Metadata["service"] != "test" {
t.Errorf("Expected service=test in metadata, got %s", rec.Metadata["service"])
}
if rec.Metadata["version"] != "1.0" {
t.Errorf("Expected version=1.0 in metadata, got %s", rec.Metadata["version"])
}
break
}
}
if !found {
t.Error("Did not find message with fields in debug log")
}
}
+104 -71
View File
@@ -3,11 +3,14 @@ package logger
import (
"context"
"fmt"
"log/slog"
"os"
"runtime"
"sort"
"strings"
"sync"
"time"
dlog "go-micro.dev/v5/debug/log"
)
func init() {
@@ -21,47 +24,15 @@ func init() {
type defaultLogger struct {
opts Options
slog *slog.Logger
sync.RWMutex
}
// Init (opts...) should only overwrite provided options.
func (l *defaultLogger) Init(opts ...Option) error {
l.Lock()
defer l.Unlock()
for _, o := range opts {
o(&l.opts)
}
// Recreate slog logger with new options
handlerOpts := &slog.HandlerOptions{
Level: l.opts.Level.ToSlog(),
AddSource: true,
}
// Create text handler for stdout
textHandler := slog.NewTextHandler(l.opts.Out, handlerOpts)
// Create debug log handler for debug/log buffer
debugHandler := newDebugLogHandler(handlerOpts.Level)
// Combine both handlers
handler := newMultiHandler(textHandler, debugHandler)
l.slog = slog.New(handler)
// Add fields if any
if len(l.opts.Fields) > 0 {
const fieldsPerKV = 2
args := make([]any, 0, len(l.opts.Fields)*fieldsPerKV)
for k, v := range l.opts.Fields {
args = append(args, k, v)
}
l.slog = l.slog.With(args...)
}
return nil
}
@@ -70,24 +41,25 @@ func (l *defaultLogger) String() string {
}
func (l *defaultLogger) Fields(fields map[string]interface{}) Logger {
l.RLock()
nfields := copyFields(l.opts.Fields)
opts := l.opts
l.RUnlock()
l.Lock()
nfields := make(map[string]interface{}, len(l.opts.Fields))
for k, v := range l.opts.Fields {
nfields[k] = v
}
l.Unlock()
for k, v := range fields {
nfields[k] = v
}
// Create new logger without locks
newLogger := NewLogger(
WithLevel(opts.Level),
WithFields(nfields),
WithOutput(opts.Out),
WithCallerSkipCount(opts.CallerSkipCount),
)
return newLogger
return &defaultLogger{opts: Options{
Level: l.opts.Level,
Fields: nfields,
Out: l.opts.Out,
CallerSkipCount: l.opts.CallerSkipCount,
Context: l.opts.Context,
}}
}
func copyFields(src map[string]interface{}) map[string]interface{} {
@@ -99,6 +71,33 @@ func copyFields(src map[string]interface{}) map[string]interface{} {
return dst
}
// logCallerfilePath returns a package/file:line description of the caller,
// preserving only the leaf directory name and file name.
func logCallerfilePath(loggingFilePath string) string {
// To make sure we trim the path correctly on Windows too, we
// counter-intuitively need to use '/' and *not* os.PathSeparator here,
// because the path given originates from Go stdlib, specifically
// runtime.Caller() which (as of Mar/17) returns forward slashes even on
// Windows.
//
// See https://github.com/golang/go/issues/3335
// and https://github.com/golang/go/issues/18151
//
// for discussion on the issue on Go side.
idx := strings.LastIndexByte(loggingFilePath, '/')
if idx == -1 {
return loggingFilePath
}
idx = strings.LastIndexByte(loggingFilePath[:idx], '/')
if idx == -1 {
return loggingFilePath
}
return loggingFilePath[idx+1:]
}
func (l *defaultLogger) Log(level Level, v ...interface{}) {
// TODO decide does we need to write message if log level not used?
if !l.opts.Level.Enabled(level) {
@@ -106,21 +105,39 @@ func (l *defaultLogger) Log(level Level, v ...interface{}) {
}
l.RLock()
slogger := l.slog
if slogger == nil {
// Fallback if not initialized
slogger = slog.Default()
}
fields := copyFields(l.opts.Fields)
l.RUnlock()
// Get caller information
var pcs [1]uintptr
fields["level"] = level.String()
runtime.Callers(l.opts.CallerSkipCount, pcs[:])
r := slog.NewRecord(time.Now(), level.ToSlog(), fmt.Sprint(v...), pcs[0])
if _, file, line, ok := runtime.Caller(l.opts.CallerSkipCount); ok {
fields["file"] = fmt.Sprintf("%s:%d", logCallerfilePath(file), line)
}
_ = slogger.Handler().Handle(context.Background(), r)
rec := dlog.Record{
Timestamp: time.Now(),
Message: fmt.Sprint(v...),
Metadata: make(map[string]string, len(fields)),
}
keys := make([]string, 0, len(fields))
for k, v := range fields {
keys = append(keys, k)
rec.Metadata[k] = fmt.Sprintf("%v", v)
}
sort.Strings(keys)
metadata := ""
for _, k := range keys {
metadata += fmt.Sprintf(" %s=%v", k, fields[k])
}
dlog.DefaultLog.Write(rec)
t := rec.Timestamp.Format("2006-01-02 15:04:05")
fmt.Printf("%s %s %v\n", t, metadata, rec.Message)
}
func (l *defaultLogger) Logf(level Level, format string, v ...interface{}) {
@@ -130,21 +147,39 @@ func (l *defaultLogger) Logf(level Level, format string, v ...interface{}) {
}
l.RLock()
slogger := l.slog
if slogger == nil {
// Fallback if not initialized
slogger = slog.Default()
}
fields := copyFields(l.opts.Fields)
l.RUnlock()
// Get caller information
var pcs [1]uintptr
fields["level"] = level.String()
runtime.Callers(l.opts.CallerSkipCount, pcs[:])
r := slog.NewRecord(time.Now(), level.ToSlog(), fmt.Sprintf(format, v...), pcs[0])
if _, file, line, ok := runtime.Caller(l.opts.CallerSkipCount); ok {
fields["file"] = fmt.Sprintf("%s:%d", logCallerfilePath(file), line)
}
_ = slogger.Handler().Handle(context.Background(), r)
rec := dlog.Record{
Timestamp: time.Now(),
Message: fmt.Sprintf(format, v...),
Metadata: make(map[string]string, len(fields)),
}
keys := make([]string, 0, len(fields))
for k, v := range fields {
keys = append(keys, k)
rec.Metadata[k] = fmt.Sprintf("%v", v)
}
sort.Strings(keys)
metadata := ""
for _, k := range keys {
metadata += fmt.Sprintf(" %s=%v", k, fields[k])
}
dlog.DefaultLog.Write(rec)
t := rec.Timestamp.Format("2006-01-02 15:04:05")
fmt.Printf("%s %s %v\n", t, metadata, rec.Message)
}
func (l *defaultLogger) Options() Options {
@@ -161,13 +196,11 @@ func (l *defaultLogger) Options() Options {
// NewLogger builds a new logger based on options.
func NewLogger(opts ...Option) Logger {
// Default options
const defaultCallerSkipCount = 2
options := Options{
Level: InfoLevel,
Fields: make(map[string]interface{}),
Out: os.Stderr,
CallerSkipCount: defaultCallerSkipCount,
CallerSkipCount: 2,
Context: context.Background(),
}
-26
View File
@@ -2,7 +2,6 @@ package logger
import (
"fmt"
"log/slog"
"os"
)
@@ -47,31 +46,6 @@ func (l Level) Enabled(lvl Level) bool {
return lvl >= l
}
// ToSlog converts our Level to slog.Level.
func (l Level) ToSlog() slog.Level {
const (
traceLevelOffset = 4
fatalLevelOffset = 4
)
switch l {
case TraceLevel:
return slog.LevelDebug - traceLevelOffset // Lower than Debug
case DebugLevel:
return slog.LevelDebug
case InfoLevel:
return slog.LevelInfo
case WarnLevel:
return slog.LevelWarn
case ErrorLevel:
return slog.LevelError
case FatalLevel:
return slog.LevelError + fatalLevelOffset // Higher than Error
default:
return slog.LevelInfo
}
}
// GetLevel converts a level string into a logger Level value.
// returns an error if the input string does not match known values.
func GetLevel(levelStr string) (Level, error) {
+4 -22
View File
@@ -159,22 +159,14 @@ func (c *cache) get(service string) ([]*registry.Service, error) {
return cp, nil
}
// Check rate limiting BEFORE entering singleflight
// This prevents blocking when we have stale cache and etcd is down
// Check rate limiting before unlocking
// This prevents multiple sequential attempts within the retry interval
lastRefresh := c.lastRefreshAttempt[service]
minimumRetryInterval := c.opts.MinimumRetryInterval
if minimumRetryInterval == 0 {
minimumRetryInterval = DefaultMinimumRetryInterval
}
// If we're being rate limited AND have stale cache, return it immediately
// This avoids blocking all goroutines when etcd has long timeout
if !lastRefresh.IsZero() && time.Since(lastRefresh) < minimumRetryInterval && len(cp) > 0 {
c.RUnlock()
// Return stale cache even if expired
return cp, nil
}
// unlock the read lock before potentially blocking operations
c.RUnlock()
@@ -183,18 +175,8 @@ func (c *cache) get(service string) ([]*registry.Service, error) {
// Use singleflight to deduplicate concurrent requests
val, err, _ := c.sg.Do(service, func() (interface{}, error) {
// Inside singleflight - only one goroutine executes this
// Re-check rate limiting inside singleflight
// (in case another goroutine just completed a refresh)
c.RLock()
currentLastRefresh := c.lastRefreshAttempt[service]
currentMinimumRetryInterval := c.opts.MinimumRetryInterval
if currentMinimumRetryInterval == 0 {
currentMinimumRetryInterval = DefaultMinimumRetryInterval
}
c.RUnlock()
if !currentLastRefresh.IsZero() && time.Since(currentLastRefresh) < currentMinimumRetryInterval {
// Apply rate limiting to prevent excessive registry calls
if !lastRefresh.IsZero() && time.Since(lastRefresh) < minimumRetryInterval {
// We're being rate limited
// Check if we have stale cache to return
c.RLock()
-75
View File
@@ -1,75 +0,0 @@
#!/bin/bash
# Install script for micro CLI
# Usage: curl -fsSL https://go-micro.dev/install.sh | sh
set -e
VERSION="${MICRO_VERSION:-latest}"
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
# Normalize architecture
case $ARCH in
x86_64|amd64) ARCH="amd64" ;;
aarch64|arm64) ARCH="arm64" ;;
armv7l) ARCH="arm" ;;
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
esac
# Normalize OS
case $OS in
darwin) OS="darwin" ;;
linux) OS="linux" ;;
*) echo "Unsupported OS: $OS"; exit 1 ;;
esac
# Determine install directory
if [ "$EUID" -eq 0 ] || [ "$(id -u)" -eq 0 ]; then
INSTALL_DIR="/usr/local/bin"
else
INSTALL_DIR="$HOME/.local/bin"
mkdir -p "$INSTALL_DIR"
fi
echo "Installing micro ${VERSION} for ${OS}/${ARCH}..."
# Download URL
if [ "$VERSION" = "latest" ]; then
URL="https://github.com/micro/go-micro/releases/latest/download/micro-${OS}-${ARCH}"
else
URL="https://github.com/micro/go-micro/releases/download/${VERSION}/micro-${OS}-${ARCH}"
fi
# Download
TMP_FILE=$(mktemp)
if command -v curl &> /dev/null; then
curl -fsSL "$URL" -o "$TMP_FILE"
elif command -v wget &> /dev/null; then
wget -q "$URL" -O "$TMP_FILE"
else
echo "Error: curl or wget required"
exit 1
fi
# Install
chmod +x "$TMP_FILE"
mv "$TMP_FILE" "$INSTALL_DIR/micro"
echo ""
echo "✓ Installed micro to $INSTALL_DIR/micro"
echo ""
# Verify
if command -v micro &> /dev/null; then
micro --version
else
echo "Note: Add $INSTALL_DIR to your PATH:"
echo " export PATH=\"\$PATH:$INSTALL_DIR\""
fi
echo ""
echo "Get started:"
echo " micro new myservice # Create a new service"
echo " micro run # Run locally"
echo " micro deploy # Deploy to server"
echo ""
+11 -7
View File
@@ -4,13 +4,15 @@ import (
"encoding/json"
"strings"
b "bytes"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
"go-micro.dev/v5/codec"
"go-micro.dev/v5/codec/bytes"
"google.golang.org/grpc"
"google.golang.org/grpc/encoding"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
type jsonCodec struct{}
@@ -18,9 +20,10 @@ type bytesCodec struct{}
type protoCodec struct{}
type wrapCodec struct{ encoding.Codec }
var protojsonMarshaler = protojson.MarshalOptions{
UseProtoNames: true,
EmitUnpopulated: false,
var jsonpbMarshaler = &jsonpb.Marshaler{
EnumsAsInts: false,
EmitDefaults: false,
OrigName: true,
}
var (
@@ -82,7 +85,8 @@ func (protoCodec) Name() string {
func (jsonCodec) Marshal(v interface{}) ([]byte, error) {
if pb, ok := v.(proto.Message); ok {
return protojsonMarshaler.Marshal(pb)
s, err := jsonpbMarshaler.MarshalToString(pb)
return []byte(s), err
}
return json.Marshal(v)
@@ -93,7 +97,7 @@ func (jsonCodec) Unmarshal(data []byte, v interface{}) error {
return nil
}
if pb, ok := v.(proto.Message); ok {
return protojson.Unmarshal(data, pb)
return jsonpb.Unmarshal(b.NewReader(data), pb)
}
return json.Unmarshal(data, v)
}
+33 -114
View File
@@ -16,25 +16,19 @@ import (
)
type ntport struct {
addrs []string
opts transport.Options
nopts nats.Options
pool *connectionPool // connection pool for clients
poolSize int
poolIdleTimeout time.Duration
mu sync.RWMutex
addrs []string
opts transport.Options
nopts nats.Options
}
type ntportClient struct {
conn *nats.Conn
pooledConn *pooledConnection // reference to pooled connection if using pool
pool *connectionPool // reference to pool to return connection on close
addr string
id string
local string
remote string
sub *nats.Subscription
opts transport.Options
conn *nats.Conn
addr string
id string
local string
remote string
sub *nats.Subscription
opts transport.Options
}
type ntportSocket struct {
@@ -73,20 +67,8 @@ func configure(n *ntport, opts ...transport.Option) {
}
natsOptions := nats.GetDefaultOptions()
if no, ok := n.opts.Context.Value(optionsKey{}).(nats.Options); ok {
natsOptions = no
}
// Set pool size (default is 1 - no pooling)
n.poolSize = 1
if poolSize, ok := n.opts.Context.Value(poolSizeKey{}).(int); ok && poolSize > 0 {
n.poolSize = poolSize
}
// Set pool idle timeout (default is 5 minutes)
n.poolIdleTimeout = 5 * time.Minute
if idleTimeout, ok := n.opts.Context.Value(poolIdleTimeoutKey{}).(time.Duration); ok {
n.poolIdleTimeout = idleTimeout
if n, ok := n.opts.Context.Value(optionsKey{}).(nats.Options); ok {
natsOptions = n
}
// transport.Options have higher priority than nats.Options
@@ -109,31 +91,6 @@ func configure(n *ntport, opts ...transport.Option) {
n.opts.Addrs = setAddrs(n.opts.Addrs)
n.nopts = natsOptions
n.addrs = n.opts.Addrs
// Initialize connection pool if size > 1
if n.poolSize > 1 && n.pool == nil {
factory := func() (*nats.Conn, error) {
opts := n.nopts
opts.Servers = n.addrs
opts.Secure = n.opts.Secure
opts.TLSConfig = n.opts.TLSConfig
// secure might not be set
if n.opts.TLSConfig != nil {
opts.Secure = true
}
return opts.Connect()
}
pool, err := newConnectionPool(n.poolSize, factory)
if err == nil {
if n.poolIdleTimeout > 0 {
pool.idleTimeout = n.poolIdleTimeout
}
n.pool = pool
}
}
}
func setAddrs(addrs []string) []string {
@@ -209,13 +166,6 @@ func (n *ntportClient) Recv(m *transport.Message) error {
func (n *ntportClient) Close() error {
n.sub.Unsubscribe()
// If using a pooled connection, return it to the pool
if n.pool != nil && n.pooledConn != nil {
return n.pool.Put(n.pooledConn)
}
// Otherwise, close the connection directly
n.conn.Close()
return nil
}
@@ -394,68 +344,37 @@ func (n *ntport) Dial(addr string, dialOpts ...transport.DialOption) (transport.
o(&dopts)
}
var c *nats.Conn
var pooledConn *pooledConnection
var err error
opts := n.nopts
opts.Servers = n.addrs
opts.Secure = n.opts.Secure
opts.TLSConfig = n.opts.TLSConfig
opts.Timeout = dopts.Timeout
// Use connection pool if available
n.mu.RLock()
hasPool := n.pool != nil
n.mu.RUnlock()
// secure might not be set
if n.opts.TLSConfig != nil {
opts.Secure = true
}
if hasPool {
pooledConn, err = n.pool.Get()
if err != nil {
return nil, err
}
c = pooledConn.Conn()
if c == nil {
n.pool.Put(pooledConn)
return nil, errors.New("invalid connection from pool")
}
} else {
// Create a new connection (original behavior)
opts := n.nopts
opts.Servers = n.addrs
opts.Secure = n.opts.Secure
opts.TLSConfig = n.opts.TLSConfig
opts.Timeout = dopts.Timeout
// secure might not be set
if n.opts.TLSConfig != nil {
opts.Secure = true
}
c, err = opts.Connect()
if err != nil {
return nil, err
}
c, err := opts.Connect()
if err != nil {
return nil, err
}
id := nats.NewInbox()
sub, err := c.SubscribeSync(id)
if err != nil {
if pooledConn != nil {
n.pool.Put(pooledConn)
} else {
c.Close()
}
return nil, err
}
client := &ntportClient{
conn: c,
pooledConn: pooledConn,
pool: n.pool,
addr: addr,
id: id,
sub: sub,
opts: n.opts,
local: id,
remote: addr,
}
return client, nil
return &ntportClient{
conn: c,
addr: addr,
id: id,
sub: sub,
opts: n.opts,
local: id,
remote: addr,
}, nil
}
func (n *ntport) Listen(addr string, listenOpts ...transport.ListenOption) (transport.Listener, error) {
-27
View File
@@ -2,15 +2,12 @@ package nats
import (
"context"
"time"
"github.com/nats-io/nats.go"
"go-micro.dev/v5/transport"
)
type optionsKey struct{}
type poolSizeKey struct{}
type poolIdleTimeoutKey struct{}
// Options allow to inject a nats.Options struct for configuring
// the nats connection.
@@ -22,27 +19,3 @@ func Options(nopts nats.Options) transport.Option {
o.Context = context.WithValue(o.Context, optionsKey{}, nopts)
}
}
// PoolSize sets the size of the connection pool.
// If set to a value > 1, the transport will use a connection pool.
// Default is 1 (no pooling).
func PoolSize(size int) transport.Option {
return func(o *transport.Options) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, poolSizeKey{}, size)
}
}
// PoolIdleTimeout sets the timeout for idle connections in the pool.
// Connections idle for longer than this duration will be closed.
// Default is 5 minutes. Set to 0 to disable idle timeout.
func PoolIdleTimeout(timeout time.Duration) transport.Option {
return func(o *transport.Options) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, poolIdleTimeoutKey{}, timeout)
}
}
-188
View File
@@ -1,188 +0,0 @@
package nats
import (
"errors"
"sync"
"time"
natsp "github.com/nats-io/nats.go"
)
var (
// ErrPoolExhausted is returned when no connections are available in the pool
ErrPoolExhausted = errors.New("connection pool exhausted")
// ErrPoolClosed is returned when trying to use a closed pool
ErrPoolClosed = errors.New("connection pool is closed")
)
// connectionPool manages a pool of NATS connections
type connectionPool struct {
mu sync.RWMutex
connections chan *pooledConnection
factory func() (*natsp.Conn, error)
size int
idleTimeout time.Duration
closed bool
}
// pooledConnection wraps a NATS connection with metadata
type pooledConnection struct {
conn *natsp.Conn
createdAt time.Time
lastUsed time.Time
mu sync.Mutex
}
// newConnectionPool creates a new connection pool
func newConnectionPool(size int, factory func() (*natsp.Conn, error)) (*connectionPool, error) {
if size <= 0 {
size = 1
}
pool := &connectionPool{
connections: make(chan *pooledConnection, size),
factory: factory,
size: size,
idleTimeout: 5 * time.Minute,
closed: false,
}
return pool, nil
}
// Get retrieves a connection from the pool or creates a new one
func (p *connectionPool) Get() (*pooledConnection, error) {
p.mu.RLock()
if p.closed {
p.mu.RUnlock()
return nil, ErrPoolClosed
}
p.mu.RUnlock()
// Try to get an existing connection from the pool
select {
case conn := <-p.connections:
// Check if connection is still valid and not idle for too long
if conn.isValid() && !conn.isExpired(p.idleTimeout) {
conn.updateLastUsed()
return conn, nil
}
// Connection is invalid or expired, close it and create a new one
conn.close()
return p.createConnection()
default:
// No connection available, create a new one
return p.createConnection()
}
}
// Put returns a connection to the pool
func (p *connectionPool) Put(conn *pooledConnection) error {
p.mu.RLock()
defer p.mu.RUnlock()
if p.closed {
return conn.close()
}
// Check if connection is still valid
if !conn.isValid() {
return conn.close()
}
conn.updateLastUsed()
// Try to return connection to pool
select {
case p.connections <- conn:
return nil
default:
// Pool is full, close the connection
return conn.close()
}
}
// Close closes all connections in the pool
func (p *connectionPool) Close() error {
p.mu.Lock()
defer p.mu.Unlock()
if p.closed {
return nil
}
p.closed = true
close(p.connections)
// Close all connections in the pool
for conn := range p.connections {
conn.close()
}
return nil
}
// createConnection creates a new pooled connection
func (p *connectionPool) createConnection() (*pooledConnection, error) {
conn, err := p.factory()
if err != nil {
return nil, err
}
return &pooledConnection{
conn: conn,
createdAt: time.Now(),
lastUsed: time.Now(),
}, nil
}
// isValid checks if the underlying NATS connection is valid
func (pc *pooledConnection) isValid() bool {
pc.mu.Lock()
defer pc.mu.Unlock()
if pc.conn == nil {
return false
}
status := pc.conn.Status()
return status == natsp.CONNECTED || status == natsp.RECONNECTING
}
// isExpired checks if the connection has been idle for too long
func (pc *pooledConnection) isExpired(timeout time.Duration) bool {
pc.mu.Lock()
defer pc.mu.Unlock()
if timeout <= 0 {
return false
}
return time.Since(pc.lastUsed) > timeout
}
// close closes the underlying NATS connection
func (pc *pooledConnection) close() error {
pc.mu.Lock()
defer pc.mu.Unlock()
if pc.conn != nil {
pc.conn.Close()
pc.conn = nil
}
return nil
}
// Conn returns the underlying NATS connection
func (pc *pooledConnection) Conn() *natsp.Conn {
pc.mu.Lock()
defer pc.mu.Unlock()
return pc.conn
}
// updateLastUsed updates the last used timestamp in a thread-safe manner
func (pc *pooledConnection) updateLastUsed() {
pc.mu.Lock()
defer pc.mu.Unlock()
pc.lastUsed = time.Now()
}
-170
View File
@@ -1,170 +0,0 @@
package nats
import (
"sync"
"testing"
"time"
natsp "github.com/nats-io/nats.go"
)
func TestTransportConnectionPool_GetPut(t *testing.T) {
// Mock factory that creates connections
connCount := 0
factory := func() (*natsp.Conn, error) {
connCount++
return nil, nil
}
pool, err := newConnectionPool(3, factory)
if err != nil {
t.Fatalf("Failed to create pool: %v", err)
}
defer pool.Close()
// Get a connection (should create one)
conn1, err := pool.Get()
if err != nil {
t.Fatalf("Failed to get connection: %v", err)
}
if conn1 == nil {
t.Fatal("Expected connection, got nil")
}
// Put it back
if err := pool.Put(conn1); err != nil {
t.Fatalf("Failed to put connection: %v", err)
}
// Get it again (should reuse the same one)
conn2, err := pool.Get()
if err != nil {
t.Fatalf("Failed to get connection: %v", err)
}
if conn2 == nil {
t.Fatal("Expected connection, got nil")
}
}
func TestTransportConnectionPool_Concurrent(t *testing.T) {
connCount := 0
mu := sync.Mutex{}
factory := func() (*natsp.Conn, error) {
mu.Lock()
connCount++
mu.Unlock()
return nil, nil
}
pool, err := newConnectionPool(5, factory)
if err != nil {
t.Fatalf("Failed to create pool: %v", err)
}
defer pool.Close()
// Simulate concurrent access
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
conn, err := pool.Get()
if err != nil {
t.Errorf("Failed to get connection: %v", err)
return
}
// Simulate some work
time.Sleep(10 * time.Millisecond)
if err := pool.Put(conn); err != nil {
t.Errorf("Failed to put connection: %v", err)
}
}()
}
wg.Wait()
// We should have created some connections
mu.Lock()
if connCount == 0 {
t.Error("Expected at least one connection to be created")
}
mu.Unlock()
}
func TestTransportConnectionPool_Close(t *testing.T) {
factory := func() (*natsp.Conn, error) {
return nil, nil
}
pool, err := newConnectionPool(3, factory)
if err != nil {
t.Fatalf("Failed to create pool: %v", err)
}
// Get a connection
conn, err := pool.Get()
if err != nil {
t.Fatalf("Failed to get connection: %v", err)
}
// Close the pool
if err := pool.Close(); err != nil {
t.Fatalf("Failed to close pool: %v", err)
}
// Put connection back to closed pool should not panic
_ = pool.Put(conn)
// Try to get from closed pool
_, err = pool.Get()
if err != ErrPoolClosed {
t.Errorf("Expected ErrPoolClosed, got: %v", err)
}
}
func TestTransportPoolConfiguration(t *testing.T) {
// Test with pool size 5
tr := NewTransport(PoolSize(5))
nt, ok := tr.(*ntport)
if !ok {
t.Fatal("Expected transport to be of type *ntport")
}
if nt.poolSize != 5 {
t.Errorf("Expected pool size 5, got %d", nt.poolSize)
}
// Test with custom idle timeout
tr2 := NewTransport(PoolSize(3), PoolIdleTimeout(10*time.Minute))
nt2, ok := tr2.(*ntport)
if !ok {
t.Fatal("Expected transport to be of type *ntport")
}
if nt2.poolSize != 3 {
t.Errorf("Expected pool size 3, got %d", nt2.poolSize)
}
if nt2.poolIdleTimeout != 10*time.Minute {
t.Errorf("Expected idle timeout 10m, got %v", nt2.poolIdleTimeout)
}
}
func TestTransportDefaultSingleConnection(t *testing.T) {
// Test that default behavior is single connection (pool size 1)
tr := NewTransport()
nt, ok := tr.(*ntport)
if !ok {
t.Fatal("Expected transport to be of type *ntport")
}
if nt.poolSize != 1 {
t.Errorf("Expected default pool size 1, got %d", nt.poolSize)
}
// With size 1, pool should not be created
if nt.pool != nil {
t.Error("Expected no pool with size 1")
}
}
+6 -35
View File
@@ -10,56 +10,27 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"log"
"math/big"
"net"
"os"
"sync"
"time"
)
var (
// Track if we've already logged the warning to avoid spam
warningOnce sync.Once
)
// Config returns a TLS config.
//
// BACKWARD COMPATIBILITY: By default, InsecureSkipVerify is true for compatibility
// with existing deployments. This maintains the existing behavior to avoid breaking
// production systems during upgrades.
//
// SECURITY WARNING: The default behavior skips certificate verification. This is
// insecure and vulnerable to man-in-the-middle attacks.
//
// To enable secure certificate verification (RECOMMENDED for production):
// - Set environment variable: MICRO_TLS_SECURE=true
// - Use SecureConfig() function directly
// - Configure TLSConfig with proper certificates
// By default, InsecureSkipVerify is true for local development.
// For production, either:
// - Set MICRO_TLS_SECURE=true with proper CA certs
// - Use a service mesh (Istio, Linkerd) for mTLS
//
// DEPRECATION NOTICE: The insecure default will be changed in a future major version (v6).
// Please migrate to secure mode by setting MICRO_TLS_SECURE=true in your environment.
// - Configure TLSConfig directly with your certs
func Config() *tls.Config {
// Check environment for explicit secure mode
// Check environment for secure mode
if os.Getenv("MICRO_TLS_SECURE") == "true" {
return &tls.Config{
InsecureSkipVerify: false,
MinVersion: tls.VersionTLS12,
}
}
// Log deprecation warning once (only if not in test environment)
if os.Getenv("IN_TRAVIS_CI") == "" {
warningOnce.Do(func() {
log.Println("[SECURITY WARNING] TLS certificate verification is disabled by default. " +
"This is insecure and will change in v6. " +
"Set MICRO_TLS_SECURE=true to enable certificate verification.")
})
}
// DEPRECATED: Default remains insecure for backward compatibility
// This will change in v6 - please migrate to secure mode
// Default: insecure for local development
return &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS12,
-103
View File
@@ -1,103 +0,0 @@
package tls
import (
"os"
"testing"
)
func TestConfig(t *testing.T) {
tests := []struct {
name string
envVar string
envValue string
wantInsecure bool
description string
}{
{
name: "default_insecure_for_backward_compatibility",
envVar: "",
envValue: "",
wantInsecure: true,
description: "Default should remain insecure for backward compatibility (will change in v6)",
},
{
name: "secure_mode_enabled",
envVar: "MICRO_TLS_SECURE",
envValue: "true",
wantInsecure: false,
description: "MICRO_TLS_SECURE=true should enable certificate verification",
},
{
name: "secure_mode_disabled",
envVar: "MICRO_TLS_SECURE",
envValue: "false",
wantInsecure: true,
description: "MICRO_TLS_SECURE=false should remain insecure",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Clean up environment
os.Unsetenv("MICRO_TLS_SECURE")
os.Unsetenv("MICRO_TLS_INSECURE")
// Suppress warning in tests
os.Setenv("IN_TRAVIS_CI", "yes")
defer os.Unsetenv("IN_TRAVIS_CI")
// Set environment variable if specified
if tt.envVar != "" {
os.Setenv(tt.envVar, tt.envValue)
defer os.Unsetenv(tt.envVar)
}
config := Config()
if config == nil {
t.Fatal("Config() returned nil")
}
if config.InsecureSkipVerify != tt.wantInsecure {
t.Errorf("%s: InsecureSkipVerify = %v, want %v",
tt.description, config.InsecureSkipVerify, tt.wantInsecure)
}
// Verify MinVersion is set correctly
if config.MinVersion == 0 {
t.Error("MinVersion should be set")
}
})
}
}
func TestSecureConfig(t *testing.T) {
config := SecureConfig()
if config == nil {
t.Fatal("SecureConfig() returned nil")
}
if config.InsecureSkipVerify {
t.Error("SecureConfig should have InsecureSkipVerify set to false")
}
if config.MinVersion == 0 {
t.Error("MinVersion should be set")
}
}
func TestInsecureConfig(t *testing.T) {
config := InsecureConfig()
if config == nil {
t.Fatal("InsecureConfig() returned nil")
}
if !config.InsecureSkipVerify {
t.Error("InsecureConfig should have InsecureSkipVerify set to true")
}
if config.MinVersion == 0 {
t.Error("MinVersion should be set")
}
}