# Full example: assembling a service as a module

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

Wire the public packages into one runnable service — after `go get`, no
scaffold. For each package's usage see [Components](https://gortexa.sheng.page/en/components/). Fetch the
module:

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

How each component is enabled (all driven by config):

- **config**: `MustBuild` layers `etc/config.yaml` and the environment (`GORTEXA_` prefix).
- **observability**: exports OTLP only when `observ.tracing_otlp` / `metrics_otlp` / `logs_otlp` is set; a no-op otherwise.
- **auth**: set `auth.jwt_secret` (≥ 32 bytes) and `auth.issuer`.
- **interceptor**: `NewSet` builds the fixed eight-stage chain.
- **health**: `app.Health().Register` adds a check that `/readyz` aggregates.
- **gateway (HTTP/JSON)**: `httpcompat.NewServeMux` plus your gateway handler.
- **mcp**: `mcp.NewBridge` turns `gortexa.ai.v1`-annotated RPCs into MCP tools.
- **mq / cache / storage / client**: once `mq.url` / `cache.driver: redis` / `db.dsn` are set, use `mq.New` / `cache.New` / `storage.NewPool` / `client.NewGRPCConn` inside 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.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" // 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,
	//    and on a missing etc/config.yaml (drop WithConfigFile to skip the file).
	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, Skip: func(method string) bool { return strings.HasPrefix(method, "/grpc.health.") }},
	})
	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", // your own generated service
	}
}
```
