完整範例:以模組組裝服務
把框架當模組,用公開組件接成一支可直接起步的服務——go get 後不走 scaffold。逐組件用法見組件用法。先取得模組:
go get github.com/yshengliao/gortexa@v0.27.0各組件怎麼開啟(都由 config 決定):
- config:
MustBuild分層讀etc/config.yaml+ 環境變數(GORTEXA_前綴)。 - observability:設
observ.tracing_otlp/metrics_otlp/logs_otlp才匯出 OTLP,否則 no-op。 - auth:設
auth.jwt_secret(≥ 32 bytes)與auth.issuer。 - interceptor:
NewSet產出固定八段鏈。 - health:
app.Health().Register註冊 check,/readyz聚合。 - gateway(HTTP/JSON):
httpcompat.NewServeMux+ 你的 gateway handler。 - mcp:
mcp.NewBridge把標了gortexa.ai.v1annotations 的 RPC 變成 MCP tools。 - mq/cache/storage/client:設好
mq.url/cache.driver: redis/db.dsn後,在 handler 內用mq.New/cache.New/storage.NewPool/client.NewGRPCConn。
完整 main.go(你自己的 generated service 以註解 placeholder 標示;本片段的框架接線對 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" // 你自己 gen 出來的服務)
func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, "fatal:", err) os.Exit(1) }}
func run() error { // 1. Config —— 分層:預設 < YAML < .env < 環境變數(GORTEXA_ 前綴)。 // jwt_secret 缺漏/過短或 issuer 空白時 MustBuild 直接 fail-loud。 cfg := config.MustBuild(config.WithConfigFile("etc/config.yaml"))
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop()
// 2. Observability —— 日誌/追蹤/指標。各自只在有設 observ.*_otlp endpoint // 時匯出 OTLP,否則為 no-op;各回一個離場時 flush 緩衝的 ShutdownFunc。 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 —— 以設定的 secret/issuer/audience 建 HS256 verifier。 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. 攔截器鏈 —— recover → request-id → logger → load-shed → rate-limit → // circuit-breaker → auth → validation。health 探測不帶 token,故豁免 auth。 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 —— App 擁有單一 h2c port。StatsHandler 承載 OTel(非 interceptor); // HTTPWrap 在 HTTP 面外層加 CORS;shutdown hooks 於離場時 flush 三個 telemetry provider。 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. 註冊你 gen 出來的 gRPC 服務,加一個 /readyz 會聚合的 health check。 // StartMetricsExport 把各組件健康狀態輸出成 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 —— gateway 與 MCP bridge 都 dial 行程內 server,所以 HTTP/JSON // 與 MCP 的呼叫和 gRPC 走同一條攔截器鏈。 conn, err := app.Loopback() if err != nil { return fmt.Errorf("loopback: %w", err) }
// 8. HTTP/JSON(grpc-gateway)。註冊你服務的 gateway handler,再把 body 上限壓到 1 MiB。 gateway := httpcompat.NewServeMux(apperr.Default) // _ = billingpb.RegisterInvoiceServiceHandler(ctx, gateway, conn) app.SetGateway(httpcompat.MaxBodyBytes(gateway))
// 9. MCP bridge —— 把清單內服務中標了 gortexa.ai.v1 annotations 的 RPC 都變成 // MCP tool。SetAllowedOrigins 是 DNS-rebinding 防護。 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. 跑到 SIGINT/SIGTERM;graceful shutdown 排空進行中的工作並跑 shutdown hooks。 // 內建組件 —— mq.New(cfg.MQ)、cache.New(cfg.Cache)、 // storage.NewPool(ctx, cfg.DB, tracer)、client.NewGRPCConn(...) —— 依你的 // handler 需要在此接上。 log.Info("starting", "addr", cfg.Server.Addr) return app.Run(ctx)}
// mcpServices 列出經 MCP bridge(與 schema export)暴露的服務。func mcpServices() []protoreflect.FullName { return []protoreflect.FullName{ "billing.v1.InvoiceService", }}