# 完整範例：以模組組裝服務

Source: https://gortexa.sheng.page/examples/full-example/

把框架當模組，用公開元件接成一支可直接起步的服務——`go get` 後不走 scaffold。逐元件用法見[元件用法](https://gortexa.sheng.page/components/)。先取得模組：

```bash
go get github.com/yshengliao/gortexa@v0.28.3
```

各元件怎麼開啟（都由 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.v1` annotations 的 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.28.3 編譯驗證過）：

```go
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 空白，或 etc/config.yaml 不存在時
	//    MustBuild 直接 fail-loud（不需要設定檔就拿掉 WithConfigFile）。
	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, Skip: func(method string) bool { return strings.HasPrefix(method, "/grpc.health.") }},
	})
	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", // 你自己產生的服務
	}
}
```
