dev UX optimisations

This commit is contained in:
Asim Aslam
2026-02-04 14:12:59 +00:00
parent 29ea3a21d6
commit bab115a0bf
24 changed files with 867 additions and 516 deletions
+14 -7
View File
@@ -26,19 +26,26 @@ A clear and concise description of what you expected to happen.
## Environment
- Go Micro version: [e.g. v5.3.0]
- Go version: [e.g. 1.21.0]
- OS: [e.g. Ubuntu 22.04]
- Plugins used: [e.g. consul registry, nats broker]
- Go version: [run `go version`]
- OS/Platform: [e.g. Ubuntu 22.04, macOS 14, Docker]
- Plugins/Integrations: [e.g. consul registry, nats broker, redis cache]
## Logs
```
Paste relevant logs here
Paste relevant logs here (use -v flag for verbose output)
```
## Checklist
- [ ] I've searched existing issues and this is not a duplicate
- [ ] I've provided a minimal code sample that reproduces the issue
- [ ] I've included my environment details
- [ ] I've checked the documentation
## Additional context
Add any other context about the problem here.
## Resources
- [Documentation](https://github.com/micro/go-micro/tree/master/internal/website/docs)
- [Examples](https://github.com/micro/go-micro/tree/master/internal/website/docs/examples)
## Helpful Resources
- [Troubleshooting Guide](https://github.com/micro/go-micro/tree/master/internal/website/docs/getting-started.md)
- [Examples](https://github.com/micro/go-micro/tree/master/examples)
- [API Reference](https://pkg.go.dev/go-micro.dev/v5)
- [Discord Community](https://discord.gg/jwTYuUVAGh)
+17 -5
View File
@@ -18,13 +18,25 @@ A clear and concise description of any alternative solutions or features you've
## Use case
Describe how this feature would be used in practice. What problem does it solve?
**Example:**
```go
// Show how the feature would be used
```
## Implementation ideas (optional)
If you have thoughts on how this could be implemented, share them here.
## Additional context
Add any other context, code examples, or screenshots about the feature request here.
## Willing to contribute?
- [ ] I'd be willing to submit a PR for this feature
## Checklist
- [ ] I've searched existing issues and this is not a duplicate
- [ ] I've checked the roadmap and this isn't already planned
- [ ] I've provided a clear use case
- [ ] I'd be willing to submit a PR for this feature (optional)
## Resources
- [Documentation](https://github.com/micro/go-micro/tree/master/internal/website/docs)
- [Plugins](https://github.com/micro/go-micro/tree/master/internal/website/docs/plugins.md)
## Helpful Resources
- [Roadmap](https://github.com/micro/go-micro/blob/master/ROADMAP.md)
- [Contributing Guide](https://github.com/micro/go-micro/blob/master/CONTRIBUTING.md)
- [Architecture Docs](https://github.com/micro/go-micro/tree/master/internal/website/docs/architecture.md)
- [Discord Community](https://discord.gg/jwTYuUVAGh)
+61
View File
@@ -0,0 +1,61 @@
---
name: Performance issue
about: Report a performance problem or regression
title: '[PERFORMANCE] '
labels: performance
assignees: ''
---
## Performance Issue
**Symptom:**
Describe the performance problem (e.g., high latency, memory leak, CPU usage)
**Expected Performance:**
What performance did you expect?
## Benchmarks
Please provide benchmarks or profiling data:
```bash
# CPU profiling
go test -cpuprofile=cpu.prof -bench=.
# Memory profiling
go test -memprofile=mem.prof -bench=.
# Results
```
**Before/After comparison (if applicable):**
- Before: X req/sec, Y ms latency
- After: X req/sec, Y ms latency
## Code Sample
```go
// Minimal code that demonstrates the performance issue
```
## Environment
- Go Micro version: [e.g. v5.3.0]
- Go version: [run `go version`]
- Hardware: [e.g. 4 CPU, 8GB RAM]
- OS: [e.g. Ubuntu 22.04]
- Load: [e.g. 1000 req/sec, 100 concurrent connections]
## Profiling Data
Attach pprof profiles if available:
- CPU profile
- Memory profile
- Goroutine dump
## Additional Context
Add any other context about the performance issue.
## Resources
- [Performance Guide](https://github.com/micro/go-micro/tree/master/internal/website/docs/performance.md)
- [Benchmarking](https://pkg.go.dev/testing#hdr-Benchmarks)
+4 -1
View File
@@ -1,8 +1,11 @@
# Develop tools
/.vscode/
/.idea/
/.trunk
# VS Code workspace files (keep settings for consistency)
/.vscode/*
!/.vscode/settings.json
# Binaries for programs and plugins
*.exe
*.exe~
-29
View File
@@ -1,29 +0,0 @@
labelType: long
coverThreshold: 70
buildStyle:
bold: true
foreground: yellow
startStyle:
foreground: lightBlack
passStyle:
foreground: green
failStyle:
bold: true
foreground: "#821515"
skipStyle:
foreground: lightBlack
passPackageStyle:
foreground: green
hide: false
failPackageStyle:
bold: true
foreground: "#821515"
coveredStyle:
foreground: green
uncoveredStyle:
bold: true
foreground: yellow
fileStyle:
foreground: cyan
lineStyle:
foreground: magenta
+137
View File
@@ -0,0 +1,137 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"go.toolsManagement.autoUpdate": true,
"go.useLanguageServer": true,
"go.lintOnSave": "workspace",
"go.lintTool": "golangci-lint",
"go.lintFlags": [
"--fast"
],
"go.formatTool": "goimports",
"go.formatFlags": [],
"go.buildOnSave": "workspace",
"go.testOnSave": false,
"go.coverOnSave": false,
"go.testFlags": ["-v", "-race"],
"go.testTimeout": "60s",
"go.gopath": "",
"go.goroot": "",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
},
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
"**/node_modules": true,
"**/*.test": true,
"**/coverage.out": true,
"**/coverage.html": true
},
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/node_modules/**": true,
"**/.vscode/**": true
},
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/*.code-search": true,
"**/vendor": true,
"**/.git": true
},
"[go]": {
"editor.tabSize": 4,
"editor.insertSpaces": false,
"editor.formatOnSave": true,
"editor.defaultFormatter": "golang.go"
},
"[go.mod]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "golang.go"
},
"[markdown]": {
"editor.formatOnSave": false,
"editor.wordWrap": "on"
},
"gopls": {
"ui.semanticTokens": true,
"ui.completion.usePlaceholders": true,
"formatting.gofumpt": false,
"analyses": {
"unusedparams": true,
"shadow": true,
"fieldalignment": false
}
}
},
"extensions": {
"recommendations": [
"golang.go",
"editorconfig.editorconfig",
"redhat.vscode-yaml",
"ms-vscode.makefile-tools"
]
},
"tasks": {
"version": "2.0.0",
"tasks": [
{
"label": "Run Tests",
"type": "shell",
"command": "make test",
"group": {
"kind": "test",
"isDefault": true
},
"presentation": {
"reveal": "always",
"panel": "new"
}
},
{
"label": "Run Tests with Coverage",
"type": "shell",
"command": "make test-coverage",
"group": "test"
},
{
"label": "Run Linter",
"type": "shell",
"command": "make lint",
"group": "build"
},
{
"label": "Format Code",
"type": "shell",
"command": "make fmt",
"group": "build"
}
]
},
"launch": {
"version": "0.2.0",
"configurations": [
{
"name": "Debug Current File",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${file}"
},
{
"name": "Debug Test",
"type": "go",
"request": "launch",
"mode": "test",
"program": "${workspaceFolder}"
}
]
}
}
+4
View File
@@ -91,6 +91,10 @@ go test -v ./...
# Run specific test
go test -run TestMyFunction ./pkg/...
# Optional: Use richgo for colored output
go install github.com/kyoh86/richgo@latest
richgo test -v ./...
```
### Documentation
+8
View File
@@ -108,6 +108,14 @@ curl -XPOST \
http://localhost:8080
```
## Examples
Check out [/examples](examples/) for runnable code:
- [hello-world](examples/hello-world/) - Basic RPC service
- [web-service](examples/web-service/) - HTTP REST API
See [all examples](examples/README.md) for more.
## Protobuf
Install the code generator and see usage in the docs:
+179
View File
@@ -0,0 +1,179 @@
# Security Policy
## Supported Versions
We actively support the following versions of go-micro:
| Version | Supported |
| ------- | ------------------ |
| 5.x | :white_check_mark: |
| 4.x | :x: |
| 3.x | :x: |
| < 3.0 | :x: |
## Reporting a Vulnerability
**Please do not report security vulnerabilities through public GitHub issues.**
### How to Report
Send security vulnerability reports to: **security@go-micro.dev**
Or use GitHub's private security advisory feature:
https://github.com/micro/go-micro/security/advisories/new
### What to Include
Please include as much of the following information as possible:
- Type of vulnerability (e.g., RCE, XSS, SQL injection, etc.)
- Full paths of source file(s) related to the vulnerability
- Location of the affected source code (tag/branch/commit or direct URL)
- Step-by-step instructions to reproduce the issue
- Proof-of-concept or exploit code (if possible)
- Impact of the issue, including how an attacker might exploit it
### Response Timeline
- **Acknowledgment**: Within 48 hours
- **Initial Assessment**: Within 5 business days
- **Fix Timeline**: Depends on severity
- Critical: 7 days
- High: 14 days
- Medium: 30 days
- Low: Next release cycle
### Disclosure Policy
- We follow **coordinated disclosure**
- We'll work with you to understand and fix the issue
- We'll credit you in the security advisory (unless you prefer to remain anonymous)
- Please give us reasonable time to fix before public disclosure
- We'll publish a security advisory on GitHub when the fix is released
## Security Best Practices
When using go-micro in production:
### TLS/Transport Security
```go
import "go-micro.dev/v5/transport"
// Enable TLS verification (recommended)
os.Setenv("MICRO_TLS_SECURE", "true")
// Or use SecureConfig explicitly
tlsConfig := transport.SecureConfig()
```
See [TLS Security Update](internal/website/docs/TLS_SECURITY_UPDATE.md) for details.
### Authentication
```go
import "go-micro.dev/v5/auth"
// Use JWT authentication
service := micro.NewService(
micro.Auth(auth.NewAuth()),
)
```
### Input Validation
Always validate and sanitize inputs in your handlers:
```go
func (h *Handler) Create(ctx context.Context, req *Request, rsp *Response) error {
// Validate input
if req.Name == "" {
return errors.BadRequest("handler.create", "name is required")
}
// Sanitize and process
// ...
}
```
### Rate Limiting
Implement rate limiting for public-facing services:
```go
import "go-micro.dev/v5/client"
// Client-side rate limiting
client.NewClient(
client.RequestTimeout(time.Second * 5),
client.Retries(3),
)
```
### Secrets Management
Never commit secrets to version control:
```go
// Good: Use environment variables
apiKey := os.Getenv("API_KEY")
// Better: Use a secrets manager
import "github.com/hashicorp/vault/api"
```
### Dependency Security
Regularly update dependencies:
```bash
# Check for vulnerabilities
go list -json -m all | nancy sleuth
# Update dependencies
go get -u ./...
go mod tidy
```
## Known Security Considerations
### Reflection Usage
go-micro uses reflection for automatic handler registration. While this is a deliberate design choice for developer productivity, be aware:
- Type safety is enforced at runtime, not compile time
- Malformed requests won't crash services (errors are returned)
- See [Performance Considerations](internal/website/docs/performance.md)
### TLS Certificate Verification
**Default behavior in v5**: TLS certificate verification is **disabled** for backward compatibility.
**Production recommendation**: Enable secure mode:
```bash
export MICRO_TLS_SECURE=true
```
This will be the default in v6.
## Security Updates
Security updates are published as:
- GitHub Security Advisories
- Release notes with `[SECURITY]` prefix
- CVE entries for critical issues
Subscribe to releases: https://github.com/micro/go-micro/releases
## Bug Bounty
We currently do not offer a bug bounty program, but we greatly appreciate responsible disclosure and will publicly credit researchers who report valid security issues.
## Questions?
For security questions that are not vulnerabilities, please:
- Open a discussion: https://github.com/micro/go-micro/discussions
- Join Discord: https://discord.gg/jwTYuUVAGh
- Email: support@go-micro.dev
+3 -1
View File
@@ -108,7 +108,9 @@ func (m *mem) Consume(topic string, opts ...ConsumeOption) (<-chan Event, error)
for _, o := range opts {
o(&options)
}
// TODO RetryLimit
// Note: RetryLimit is configured but retry logic is basic for the in-memory implementation.
// For production use with advanced retry capabilities, use NATS JetStream.
// setup the subscriber
sub := &subscriber{
+98
View File
@@ -0,0 +1,98 @@
# Go Micro Examples
This directory contains runnable examples demonstrating various go-micro features and patterns.
## Quick Start
Each example can be run with `go run .` from its directory.
## Examples
### [hello-world](./hello-world/)
Basic RPC service demonstrating core concepts:
- Service creation and registration
- Handler implementation
- Client calls
- Health checks
**Run it:**
```bash
cd hello-world
go run .
```
### [pubsub-events](./pubsub-events/)
Event-driven architecture with NATS:
- Publishing events
- Subscribing to topics
- Event handlers
- Asynchronous processing
**Run it:**
```bash
cd pubsub-events
go run publisher/main.go # Terminal 1
go run subscriber/main.go # Terminal 2
```
### [web-service](./web-service/)
HTTP web service with service discovery:
- HTTP handlers
- Service registration
- Health checks
- Static file serving
**Run it:**
```bash
cd web-service
go run .
```
### [grpc-integration](./grpc-integration/)
Using go-micro with gRPC:
- Protocol buffer definitions
- gRPC client/server
- Code generation
- Type-safe APIs
**Run it:**
```bash
cd grpc-integration
make proto # Generate code
go run server/main.go # Terminal 1
go run client/main.go # Terminal 2
```
### [production-ready](./production-ready/)
Complete production-grade service:
- Structured logging
- Metrics and tracing
- Health checks
- Graceful shutdown
- Configuration management
- Error handling
**Run it:**
```bash
cd production-ready
go run .
```
## Prerequisites
Some examples require external dependencies:
- **NATS**: `docker run -p 4222:4222 nats:latest`
- **Consul**: `docker run -p 8500:8500 consul:latest agent -dev -ui -client=0.0.0.0`
- **Redis**: `docker run -p 6379:6379 redis:latest`
## Contributing
To add a new example:
1. Create a new directory
2. Add a descriptive README.md
3. Include working code with comments
4. Add to this index
5. Ensure it runs with `go run .`
+62
View File
@@ -0,0 +1,62 @@
# Hello World Example
The simplest go-micro service demonstrating core concepts.
## What It Does
This example creates a basic RPC service that:
- Listens on port 8080
- Exposes a `Greeter.Hello` method
- Returns a greeting message
- Demonstrates both programmatic and HTTP access
## Run It
```bash
go run main.go
```
The service will start and make test calls to itself, then wait for incoming requests.
## Test It
### Using curl
```bash
curl -X POST http://localhost:8080 \
-H 'Content-Type: application/json' \
-H 'Micro-Endpoint: Greeter.Hello' \
-d '{"name": "Alice"}'
```
Expected response:
```json
{"message": "Hello Alice"}
```
### Using the micro CLI
```bash
micro call greeter Greeter.Hello '{"name": "Bob"}'
```
## Code Walkthrough
1. **Define types** - Request and Response structures
2. **Implement handler** - The `Greeter` service with `Hello` method
3. **Create service** - Using `micro.New()` with options
4. **Register handler** - Link the handler to the service
5. **Run service** - Start listening for requests
## Key Concepts
- **RPC Pattern**: Method signature `func(ctx, req, rsp) error`
- **Service Discovery**: Automatic registration
- **Multiple Transports**: Works over HTTP, gRPC, etc.
- **Type Safety**: Strongly typed requests/responses
## Next Steps
- See [pubsub-events](../pubsub-events/) for event-driven patterns
- See [production-ready](../production-ready/) for a complete example
- Read the [Getting Started Guide](../../internal/website/docs/getting-started.md)
+7
View File
@@ -0,0 +1,7 @@
module example
go 1.24
require go-micro.dev/v5 latest
replace go-micro.dev/v5 => ../..
+90
View File
@@ -0,0 +1,90 @@
package main
import (
"context"
"fmt"
"log"
"go-micro.dev/v5"
"go-micro.dev/v5/client"
)
// Request and Response types
type Request struct {
Name string `json:"name"`
}
type Response struct {
Message string `json:"message"`
}
// Greeter service handler
type Greeter struct{}
// Hello is the RPC method handler
func (g *Greeter) Hello(ctx context.Context, req *Request, rsp *Response) error {
rsp.Message = "Hello " + req.Name
log.Printf("Received request: %s", req.Name)
return nil
}
func main() {
// Create a new service
service := micro.New(
micro.Name("greeter"),
micro.Version("latest"),
micro.Address(":8080"),
)
// Initialize the service
service.Init()
// Register the handler
if err := service.Handle(new(Greeter)); err != nil {
log.Fatal(err)
}
// Run the service in a goroutine
go func() {
if err := service.Run(); err != nil {
log.Fatal(err)
}
}()
// Wait for service to start
fmt.Println("Service started on :8080")
fmt.Println("Testing the service...")
// Create a client to test the service
c := service.Client()
// Make a request
req := c.NewRequest("greeter", "Greeter.Hello", &Request{Name: "World"})
rsp := &Response{}
if err := c.Call(context.Background(), req, rsp); err != nil {
log.Printf("Error calling service: %v", err)
} else {
fmt.Printf("Response: %s\n", rsp.Message)
}
// Make another request
req2 := c.NewRequest("greeter", "Greeter.Hello", &Request{Name: "Go Micro"})
rsp2 := &Response{}
if err := c.Call(context.Background(), req2, rsp2); err != nil {
log.Printf("Error calling service: %v", err)
} else {
fmt.Printf("Response: %s\n", rsp2.Message)
}
// Test with HTTP client
fmt.Println("\nYou can also test with curl:")
fmt.Println("curl -X POST http://localhost:8080 \\")
fmt.Println(" -H 'Content-Type: application/json' \\")
fmt.Println(" -H 'Micro-Endpoint: Greeter.Hello' \\")
fmt.Println(" -d '{\"name\": \"Alice\"}'")
// Keep service running
select {}
}
+59
View File
@@ -0,0 +1,59 @@
# Web Service Example
HTTP web service with automatic service discovery and registration.
## What It Does
This example creates an HTTP service that:
- Serves RESTful API endpoints
- Registers with service discovery
- Provides health checks
- Uses standard Go HTTP handlers
## Run It
```bash
go run main.go
```
## Test It
```bash
# Get service info
curl http://localhost:9090/
# List all users
curl http://localhost:9090/users
# Get specific user
curl http://localhost:9090/users/1
# Health check
curl http://localhost:9090/health
```
## Key Features
- **Standard HTTP**: Use familiar `http.Handler` interface
- **Service Discovery**: Automatically registers with registry
- **Health Checks**: Built-in health endpoint
- **JSON APIs**: Easy REST API development
## When to Use
Use `web.Service` when:
- Building REST APIs
- Serving web UIs
- Working with HTTP-specific features
- Migrating existing HTTP services
Use regular `micro.Service` when:
- Building RPC services
- Need bidirectional streaming
- Want automatic load balancing
- Prefer structured RPC over HTTP
## Next Steps
- See [hello-world](../hello-world/) for RPC services
- See [production-ready](../production-ready/) for observability
+7
View File
@@ -0,0 +1,7 @@
module example
go 1.24
require go-micro.dev/v5 latest
replace go-micro.dev/v5 => ../..
+102
View File
@@ -0,0 +1,102 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
"go-micro.dev/v5/web"
)
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
var users = map[string]*User{
"1": {ID: "1", Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now()},
"2": {ID: "2", Name: "Bob", Email: "bob@example.com", CreatedAt: time.Now()},
}
func main() {
// Create a new web service
service := web.NewService(
web.Name("web.service"),
web.Version("latest"),
web.Address(":9090"),
)
// Initialize
service.Init()
// Register handlers
service.HandleFunc("/", homeHandler)
service.HandleFunc("/users", usersHandler)
service.HandleFunc("/users/", userHandler)
service.HandleFunc("/health", healthHandler)
fmt.Println("Web service starting on :9090")
fmt.Println("Try:")
fmt.Println(" curl http://localhost:9090/")
fmt.Println(" curl http://localhost:9090/users")
fmt.Println(" curl http://localhost:9090/users/1")
fmt.Println(" curl http://localhost:9090/health")
// Run the service
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"service": "web.service",
"version": "latest",
"status": "running",
})
}
func usersHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Return all users
userList := make([]*User, 0, len(users))
for _, user := range users {
userList = append(userList, user)
}
json.NewEncoder(w).Encode(userList)
}
func userHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Extract user ID from path
id := r.URL.Path[len("/users/"):]
user, exists := users[id]
if !exists {
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{
"error": "User not found",
})
return
}
json.NewEncoder(w).Encode(user)
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "healthy",
"timestamp": time.Now().Unix(),
"uptime": "running",
})
}
+5 -5
View File
@@ -11,11 +11,11 @@ The `testing` package provides utilities for testing micro services in isolation
```go
import (
"testing"
microtesting "go-micro.dev/v5/testing"
"go-micro.dev/v5/test"
)
func TestGreeter(t *testing.T) {
h := microtesting.NewHarness(t)
h := test.NewHarness(t)
defer h.Stop()
h.Name("greeter").Register(new(GreeterHandler))
@@ -47,7 +47,7 @@ This allows your service to run without affecting or being affected by other ser
### Creating a Harness
```go
h := microtesting.NewHarness(t)
h := test.NewHarness(t)
defer h.Stop() // Always stop to clean up
```
@@ -103,7 +103,7 @@ package users
import (
"context"
"testing"
microtesting "go-micro.dev/v5/testing"
"go-micro.dev/v5/test"
)
type UsersHandler struct {
@@ -131,7 +131,7 @@ func (h *UsersHandler) Create(ctx context.Context, req *CreateRequest, rsp *Crea
}
func TestUsersCreate(t *testing.T) {
h := microtesting.NewHarness(t)
h := test.NewHarness(t)
defer h.Stop()
handler := &UsersHandler{users: make(map[string]*User)}
-64
View File
@@ -1,64 +0,0 @@
package test
import (
"testing"
"go-micro.dev/v5"
"go-micro.dev/v5/broker"
"go-micro.dev/v5/client"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/server"
"go-micro.dev/v5/transport"
"go-micro.dev/v5/util/test"
)
func BenchmarkService(b *testing.B) {
cfg := ServiceTestConfig{
Name: "test-service",
NewService: newService,
Parallel: []int{1, 8, 16, 32, 64},
Sequential: []int{0},
Streams: []int{0},
// PubSub: []int{10},
}
cfg.Run(b)
}
func newService(name string, opts ...micro.Option) (micro.Service, error) {
r := registry.NewMemoryRegistry(
registry.Services(test.Data),
)
b := broker.NewMemoryBroker()
t := transport.NewHTTPTransport()
c := client.NewClient(
client.Transport(t),
client.Broker(b),
)
s := server.NewRPCServer(
server.Name(name),
server.Registry(r),
server.Transport(t),
server.Broker(b),
)
if err := s.Init(); err != nil {
return nil, err
}
options := []micro.Option{
micro.Name(name),
micro.Server(s),
micro.Client(c),
micro.Registry(r),
micro.Broker(b),
}
options = append(options, opts...)
srv := micro.NewService(options...)
return srv, nil
}
-392
View File
@@ -1,392 +0,0 @@
// Package test implements a testing framwork, and provides default tests.
package test
import (
"context"
"fmt"
"sync"
"testing"
"time"
"github.com/pkg/errors"
"go-micro.dev/v5"
"go-micro.dev/v5/client"
"go-micro.dev/v5/debug/handler"
pb "go-micro.dev/v5/debug/proto"
)
var (
// ErrNoTests returns no test params are set.
ErrNoTests = errors.New("No tests to run, all values set to 0")
testTopic = "Test-Topic"
errorTopic = "Error-Topic"
)
type parTest func(name string, c client.Client, p, s int, errChan chan error)
type testFunc func(name string, c client.Client, errChan chan error)
// ServiceTestConfig allows you to easily test a service configuration by
// running predefined tests against your custom service. You only need to
// provide a function to create the service, and how many of which test you
// want to run.
//
// The default tests provided, all running with separate parallel routines are:
// - Sequential Call requests
// - Bi-directional streaming
// - Pub/Sub events brokering
//
// You can provide an array of parallel routines to run for the request and
// stream tests. They will be run as matrix tests, so with each possible combination.
// Thus, in total (p * seq) + (p * streams) tests will be run.
type ServiceTestConfig struct {
// Service name to use for the tests
Name string
// NewService function will be called to setup the new service.
// It takes in a list of options, which by default will Context and an
// AfterStart with channel to signal when the service has been started.
NewService func(name string, opts ...micro.Option) (micro.Service, error)
// Parallel is the number of prallell routines to use for the tests.
Parallel []int
// Sequential is the number of sequential requests to send per parallel process.
Sequential []int
// Streams is the nummber of streaming messages to send over the stream per routine.
Streams []int
// PubSub is the number of times to publish messages to the broker per routine.
PubSub []int
mu sync.Mutex
msgCount int
}
// Run will start the benchmark tests.
func (stc *ServiceTestConfig) Run(b *testing.B) {
if err := stc.validate(); err != nil {
b.Fatal("Failed to validate config", err)
}
// Run routines with sequential requests
stc.prepBench(b, "req", stc.runParSeqTest, stc.Sequential)
// Run routines with streams
stc.prepBench(b, "streams", stc.runParStreamTest, stc.Streams)
// Run routines with pub/sub
stc.prepBench(b, "pubsub", stc.runBrokerTest, stc.PubSub)
}
// prepBench will prepare the benmark by setting the right parameters,
// and invoking the test.
func (stc *ServiceTestConfig) prepBench(b *testing.B, tName string, test parTest, seq []int) {
par := stc.Parallel
// No requests needed
if len(seq) == 0 || seq[0] == 0 {
return
}
for _, parallel := range par {
for _, sequential := range seq {
// Create the service name for the test
name := fmt.Sprintf("%s.%dp-%d%s", stc.Name, parallel, sequential, tName)
// Run test with parallel routines making each sequential requests
test := func(name string, c client.Client, errChan chan error) {
test(name, c, parallel, sequential, errChan)
}
benchmark := func(b *testing.B) {
b.ReportAllocs()
stc.runBench(b, name, test)
}
b.Logf("----------- STARTING TEST %s -----------", name)
// Run test, return if it fails
if !b.Run(name, benchmark) {
return
}
}
}
}
// runParSeqTest will make s sequential requests in p parallel routines.
func (stc *ServiceTestConfig) runParSeqTest(name string, c client.Client, p, s int, errChan chan error) {
testParallel(p, func() {
// Make serial requests
for z := 0; z < s; z++ {
if err := testRequest(context.Background(), c, name); err != nil {
errChan <- errors.Wrapf(err, "[%s] Request failed during testRequest", name)
return
}
}
})
}
// Handle is used as a test handler.
func (stc *ServiceTestConfig) Handle(ctx context.Context, msg *pb.HealthRequest) error {
stc.mu.Lock()
stc.msgCount++
stc.mu.Unlock()
return nil
}
// HandleError is used as a test handler.
func (stc *ServiceTestConfig) HandleError(ctx context.Context, msg *pb.HealthRequest) error {
return errors.New("dummy error")
}
// runBrokerTest will publish messages to the broker to test pub/sub.
func (stc *ServiceTestConfig) runBrokerTest(name string, c client.Client, p, s int, errChan chan error) {
stc.msgCount = 0
testParallel(p, func() {
for z := 0; z < s; z++ {
msg := pb.BusMsg{Msg: "Hello from broker!"}
if err := c.Publish(context.Background(), c.NewMessage(testTopic, &msg)); err != nil {
errChan <- errors.Wrap(err, "failed to publish message to broker")
return
}
msg = pb.BusMsg{Msg: "Some message that will error"}
if err := c.Publish(context.Background(), c.NewMessage(errorTopic, &msg)); err == nil {
errChan <- errors.New("Publish is supposed to return an error, but got no error")
return
}
}
})
if stc.msgCount != s*p {
errChan <- fmt.Errorf("pub/sub does not work properly, invalid message count. Expected %d messaged, but received %d", s*p, stc.msgCount)
return
}
}
// runParStreamTest will start streaming, and send s messages parallel in p routines.
func (stc *ServiceTestConfig) runParStreamTest(name string, c client.Client, p, s int, errChan chan error) {
testParallel(p, func() {
// Create a client service
srv := pb.NewDebugService(name, c)
// Establish a connection to server over which we start streaming
bus, err := srv.MessageBus(context.Background())
if err != nil {
errChan <- errors.Wrap(err, "failed to connect to message bus")
return
}
// Start streaming requests
for z := 0; z < s; z++ {
if err := bus.Send(&pb.BusMsg{Msg: "Hack the world!"}); err != nil {
errChan <- errors.Wrap(err, "failed to send to stream")
return
}
msg, err := bus.Recv()
if err != nil {
errChan <- errors.Wrap(err, "failed to receive message from stream")
return
}
expected := "Request received!"
if msg.Msg != expected {
errChan <- fmt.Errorf("stream returned unexpected mesage. Expected '%s', but got '%s'", expected, msg.Msg)
return
}
}
})
}
// validate will make sure the provided test parameters are a legal combination.
func (stc *ServiceTestConfig) validate() error {
lp, lseq, lstr := len(stc.Parallel), len(stc.Sequential), len(stc.Streams)
if lp == 0 || (lseq == 0 && lstr == 0) {
return ErrNoTests
}
return nil
}
// runBench will create a service with the provided stc.NewService function,
// and run a benchmark on the test function.
func (stc *ServiceTestConfig) runBench(b *testing.B, name string, test testFunc) {
b.StopTimer()
// Channel to signal service has started
started := make(chan struct{})
// Context with cancel to stop the service
ctx, cancel := context.WithCancel(context.Background())
opts := []micro.Option{
micro.Context(ctx),
micro.AfterStart(func() error {
started <- struct{}{}
return nil
}),
}
// Create a new service per test
service, err := stc.NewService(name, opts...)
if err != nil {
b.Fatalf("failed to create service: %v", err)
}
// Register handler
if err := pb.RegisterDebugHandler(service.Server(), handler.NewHandler(service.Client())); err != nil {
b.Fatalf("failed to register handler during initial service setup: %v", err)
}
o := service.Options()
if err := o.Broker.Connect(); err != nil {
b.Fatal(err)
}
// a := new(testService)
if err := o.Server.Subscribe(o.Server.NewSubscriber(testTopic, stc.Handle)); err != nil {
b.Fatalf("[%s] Failed to register subscriber: %v", name, err)
}
if err := o.Server.Subscribe(o.Server.NewSubscriber(errorTopic, stc.HandleError)); err != nil {
b.Fatalf("[%s] Failed to register subscriber: %v", name, err)
}
b.Logf("# == [ Service ] ==================")
b.Logf("# * Server: %s", o.Server.String())
b.Logf("# * Client: %s", o.Client.String())
b.Logf("# * Transport: %s", o.Transport.String())
b.Logf("# * Broker: %s", o.Broker.String())
b.Logf("# * Registry: %s", o.Registry.String())
b.Logf("# * Auth: %s", o.Auth.String())
b.Logf("# * Cache: %s", o.Cache.String())
b.Logf("# ================================")
RunBenchmark(b, name, service, test, cancel, started)
}
// RunBenchmark will run benchmarks on a provided service.
//
// A test function can be provided that will be fun b.N times.
func RunBenchmark(b *testing.B, name string, service micro.Service, test testFunc,
cancel context.CancelFunc, started chan struct{}) {
b.StopTimer()
// Receive errors from routines on this channel
errChan := make(chan error, 1)
// Receive singal after service has shutdown
done := make(chan struct{})
// Start the server
go func() {
b.Logf("[%s] Starting server for benchmark", name)
if err := service.Run(); err != nil {
errChan <- errors.Wrapf(err, "[%s] Error occurred during service.Run", name)
}
done <- struct{}{}
}()
sigTerm := make(chan struct{})
// Benchmark routine
go func() {
defer func() {
b.StopTimer()
// Shutdown service
b.Logf("[%s] Shutting down", name)
cancel()
// Wait for service to be fully stopped
<-done
sigTerm <- struct{}{}
}()
// Wait for service to start
<-started
// Give the registry more time to setup
time.Sleep(time.Second)
b.Logf("[%s] Server started", name)
// Make a test call to warm the cache
for i := 0; i < 10; i++ {
if err := testRequest(context.Background(), service.Client(), name); err != nil {
errChan <- errors.Wrapf(err, "[%s] Failure during cache warmup testRequest", name)
}
}
// Check registration
services, err := service.Options().Registry.GetService(name)
if err != nil || len(services) == 0 {
errChan <- fmt.Errorf("service registration must have failed (%d services found), unable to get service: %w", len(services), err)
return
}
// Start benchmark
b.Logf("[%s] Starting benchtest", name)
b.ResetTimer()
b.StartTimer()
// Number of iterations
for i := 0; i < b.N; i++ {
test(name, service.Client(), errChan)
}
}()
// Wait for completion or catch any errors
select {
case err := <-errChan:
b.Fatal(err)
case <-sigTerm:
b.Logf("[%s] Completed benchmark", name)
}
}
// testParallel will run the test function in p parallel routines.
func testParallel(p int, test func()) {
// Waitgroup to wait for requests to finish
wg := sync.WaitGroup{}
// For concurrency
for j := 0; j < p; j++ {
wg.Add(1)
go func() {
defer wg.Done()
test()
}()
}
// Wait for test completion
wg.Wait()
}
// testRequest sends one test request.
// It calls the Debug.Health endpoint, and validates if the response returned
// contains the expected message.
func testRequest(ctx context.Context, c client.Client, name string) error {
req := c.NewRequest(
name,
"Debug.Health",
new(pb.HealthRequest),
)
rsp := new(pb.HealthResponse)
if err := c.Call(ctx, req, rsp); err != nil {
return err
}
if rsp.Status != "ok" {
return errors.New("service response: " + rsp.Status)
}
return nil
}
+3 -3
View File
@@ -1,4 +1,4 @@
// Package testing provides utilities for testing micro services.
// Package test provides utilities for testing micro services.
//
// Due to go-micro's global defaults, running multiple services in one process
// requires careful isolation. This package provides helpers for the common case
@@ -7,7 +7,7 @@
// Basic usage:
//
// func TestUserService(t *testing.T) {
// h := testing.NewHarness(t)
// h := test.NewHarness(t)
// defer h.Stop()
//
// // Register your service handler
@@ -23,7 +23,7 @@
// t.Fatal(err)
// }
// }
package testing
package test
import (
"context"
@@ -1,4 +1,4 @@
package testing
package test
import (
"context"
+2 -3
View File
@@ -29,14 +29,13 @@ func (h *httpTransportListener) Close() error {
func (h *httpTransportListener) Accept(fn func(Socket)) error {
// Create handler mux
// TODO: see if we should make a plugin out of the mux
mux := http.NewServeMux()
// Register our transport handler
mux.HandleFunc("/", h.newHandler(fn))
// Get optional handlers
// TODO: This needs to be documented clearer, and examples provided
// Get optional handlers from context.
// See examples/web-service for usage.
if h.ht.opts.Context != nil {
handlers, ok := h.ht.opts.Context.Value("http_handlers").(map[string]http.Handler)
if ok {
+4 -5
View File
@@ -39,9 +39,8 @@ type Options struct {
}
type DialOptions struct {
// TODO: add tls options when dialing
// Currently set in global options
// TLS options can be set via global transport options or Context.
// See SECURITY.md for TLS configuration best practices.
// Other options for implementations of the interface
// can be stored in a context
@@ -58,8 +57,8 @@ type DialOptions struct {
}
type ListenOptions struct {
// TODO: add tls options when listening
// Currently set in global options
// TLS options can be set via global transport options or Context.
// See SECURITY.md for TLS configuration best practices.
// Other options for implementations of the interface
// can be stored in a context