跳到內容

完整範例:以模組組裝服務

把框架當模組,用公開組件接成一支可直接起步的服務——go get 後不走 scaffold。逐組件用法見組件用法。先取得模組:

Terminal window
go get github.com/yshengliao/gortexa@v0.27.0

各組件怎麼開啟(都由 config 決定):

  • configMustBuild 分層讀 etc/config.yaml + 環境變數(GORTEXA_ 前綴)。
  • observability:設 observ.tracing_otlpmetrics_otlplogs_otlp 才匯出 OTLP,否則 no-op。
  • auth:設 auth.jwt_secret(≥ 32 bytes)與 auth.issuer
  • interceptorNewSet 產出固定八段鏈。
  • healthapp.Health().Register 註冊 check,/readyz 聚合。
  • gateway(HTTP/JSON)httpcompat.NewServeMux + 你的 gateway handler。
  • mcpmcp.NewBridge 把標了 gortexa.ai.v1 annotations 的 RPC 變成 MCP tools。
  • mq/cache/storage/client:設好 mq.urlcache.driver: redisdb.dsn 後,在 handler 內用 mq.Newcache.Newstorage.NewPoolclient.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",
}
}