Skip to content

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:

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

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.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",
}
}