Full example: assembling a service as a module
Wire the public packages into one runnable service — after go get, no
scaffold. For each package’s usage see Components. Fetch the
module:
go get github.com/yshengliao/gortexa@v0.27.0How each component is enabled (all driven by config):
- config:
MustBuildlayersetc/config.yamland the environment (GORTEXA_prefix). - observability: exports OTLP only when
observ.tracing_otlp/metrics_otlp/logs_otlpis set; a no-op otherwise. - auth: set
auth.jwt_secret(≥ 32 bytes) andauth.issuer. - interceptor:
NewSetbuilds the fixed eight-stage chain. - health:
app.Health().Registeradds a check that/readyzaggregates. - gateway (HTTP/JSON):
httpcompat.NewServeMuxplus your gateway handler. - mcp:
mcp.NewBridgeturnsgortexa.ai.v1-annotated RPCs into MCP tools. - mq / cache / storage / client: once
mq.url/cache.driver: redis/db.dsnare set, usemq.New/cache.New/storage.NewPool/client.NewGRPCConninside your handlers.
The complete main.go (your own generated service is a commented placeholder; the framework wiring in this snippet is compile-verified against v0.27.0):
package main
import ( "context" "fmt" "net/http" "os" "os/signal" "strings" "syscall" "time"
"google.golang.org/protobuf/reflect/protoreflect"
"github.com/yshengliao/gortexa/apperr" "github.com/yshengliao/gortexa/auth" "github.com/yshengliao/gortexa/config" "github.com/yshengliao/gortexa/health" "github.com/yshengliao/gortexa/httpcompat" "github.com/yshengliao/gortexa/interceptor" "github.com/yshengliao/gortexa/kernel" "github.com/yshengliao/gortexa/mcp" "github.com/yshengliao/gortexa/observability" // billingpb "github.com/me/myapp/gen/billing/v1" // your own generated service)
func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, "fatal:", err) os.Exit(1) }}
func run() error { // 1. Config — layered: defaults < YAML < .env < env (GORTEXA_ prefix). // MustBuild fails loud on a missing/short jwt_secret or blank issuer. cfg := config.MustBuild(config.WithConfigFile("etc/config.yaml"))
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop()
// 2. Observability — logs/traces/metrics. Each exports over OTLP only when // its observ.*_otlp endpoint is set, and is a no-op otherwise. Each // returns a ShutdownFunc that flushes buffered telemetry on exit. log, logShutdown, err := observability.SetupLogs(ctx, cfg.Log, cfg.Observ) if err != nil { return fmt.Errorf("setup logs: %w", err) } traceShutdown, err := observability.SetupTracing(ctx, cfg.Observ) if err != nil { return fmt.Errorf("setup tracing: %w", err) } metricShutdown, err := observability.SetupMetrics(ctx, cfg.Observ) if err != nil { return fmt.Errorf("setup metrics: %w", err) } govMetrics, err := observability.NewGovernanceMetrics() if err != nil { return fmt.Errorf("governance metrics: %w", err) }
// 3. Auth — HS256 verifier from the configured secret / issuer / audience. verifier, err := auth.NewVerifier([]byte(cfg.Auth.JWTSecret.Reveal()), cfg.Auth.Issuer, cfg.Auth.Audience) if err != nil { return fmt.Errorf("auth verifier: %w", err) }
// 4. Interceptor chain — recover → request-id → logger → load-shed → // rate-limit → circuit-breaker → auth → validation. Health probes carry // no token, so skip auth for them. set, err := interceptor.NewSet(interceptor.Config{ Logger: log, Verifier: verifier, Metrics: govMetrics, AuthSkip: func(method string) bool { return strings.HasPrefix(method, "/grpc.health.") }, RateLimit: interceptor.RateLimitConfig{RPS: 200, Burst: 100, TTL: 10 * time.Minute}, CircuitBreaker: interceptor.CBConfig{MaxFailures: 5, OpenInterval: 10 * time.Second, HalfOpenMax: 2}, LoadShedding: interceptor.LoadSheddingConfig{MaxInflight: 1024}, }) if err != nil { return fmt.Errorf("interceptors: %w", err) }
// 5. Kernel — the App owns the single h2c port. StatsHandler carries OTel // (not interceptors); HTTPWrap adds CORS around the HTTP surface; // shutdown hooks flush the three telemetry providers on exit. app, err := kernel.New( kernel.WithConfig(cfg), kernel.WithLogger(log), kernel.WithInterceptors(set), kernel.WithStatsHandler(observability.ServerStatsHandler()), kernel.WithHTTPWrap(func(h http.Handler) http.Handler { return httpcompat.CORS(h, cfg.Server) }), kernel.WithShutdownHook(traceShutdown), kernel.WithShutdownHook(metricShutdown), kernel.WithShutdownHook(logShutdown), ) if err != nil { return fmt.Errorf("build app: %w", err) }
// 6. Register your generated gRPC service, plus a health check that /readyz // aggregates. StartMetricsExport publishes component health as an OTel gauge. // billingpb.RegisterInvoiceServiceServer(app.GRPCServer(), billing.NewInvoiceService()) app.Health().Register("self", func(context.Context) health.State { return health.Healthy }) app.Health().StartMetricsExport(ctx, govMetrics, 0)
// 7. Loopback — the gateway and MCP bridge dial the in-process server, so // HTTP/JSON and MCP calls go through the same interceptor chain as gRPC. conn, err := app.Loopback() if err != nil { return fmt.Errorf("loopback: %w", err) }
// 8. HTTP/JSON via grpc-gateway. Register your service's gateway handler, // then cap the body at 1 MiB. gateway := httpcompat.NewServeMux(apperr.Default) // _ = billingpb.RegisterInvoiceServiceHandler(ctx, gateway, conn) app.SetGateway(httpcompat.MaxBodyBytes(gateway))
// 9. MCP bridge — exposes every gortexa.ai.v1-annotated RPC in the listed // services as an MCP tool. SetAllowedOrigins is the DNS-rebinding guard. descs, err := mcp.ServiceDescriptors(mcpServices()...) if err != nil { return fmt.Errorf("mcp descriptors: %w", err) } bridge, err := mcp.NewBridge(conn, descs, apperr.Default, cfg.Observ) if err != nil { return fmt.Errorf("mcp bridge: %w", err) } bridge.SetAllowedOrigins(cfg.Server.CORSOrigins) app.SetMCPHandler(bridge.Handler())
// 10. Run until SIGINT/SIGTERM; graceful shutdown drains in-flight work and // runs the shutdown hooks. Optional batteries — mq.New(cfg.MQ), // cache.New(cfg.Cache), storage.NewPool(ctx, cfg.DB, tracer), // client.NewGRPCConn(...) — are wired here as your handlers need them. log.Info("starting", "addr", cfg.Server.Addr) return app.Run(ctx)}
// mcpServices lists the services exposed over the MCP bridge (and schema export).func mcpServices() []protoreflect.FullName { return []protoreflect.FullName{ "billing.v1.InvoiceService", }}