builder.go
Rama master · réplica de lectura del repositorio original.
package aastro
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"net/textproto"
"strings"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/sdk/resource"
"go.uber.org/zap"
"github.com/voidrunner3074/aastro/internal/circuitbreaker"
"github.com/voidrunner3074/aastro/internal/metric"
"github.com/voidrunner3074/aastro/internal/otelcommon"
"github.com/voidrunner3074/aastro/internal/ratelimit"
"github.com/voidrunner3074/aastro/internal/tlsutil"
"github.com/voidrunner3074/aastro/internal/tracing"
)
type RoutingConfigSet struct {
Routing RoutingConfig
Service ServiceConfig
ServiceVersion string // injected via ldflags
Metrics MetricsConfig
Tracing TracingConfig
}
type RouterBundle struct {
Router *Router
TLSRegistry *tlsutil.Registry
MeterProvider otelcommon.Provider
TracerProvider otelcommon.Provider
PromRegistry *prometheus.Registry // nil unless metrics.exporter == "prometheus"
}
func NewRouter(ctx context.Context, cfgSet RoutingConfigSet, log *zap.Logger) (RouterBundle, error) {
tracing.InstallPropagator() // Sets the propagator even if tracing is disabled
res, err := otelcommon.NewResource(ctx, cfgSet.Service.Name, cfgSet.ServiceVersion)
if err != nil {
return RouterBundle{}, fmt.Errorf("build otel resource: %w", err)
}
meterProvider, promRegistry, err := initMetrics(ctx, cfgSet.Metrics, res)
if err != nil {
return RouterBundle{}, fmt.Errorf("init metrics: %w", err)
}
metrics, err := metric.New()
if err != nil {
return RouterBundle{}, fmt.Errorf("init metric instruments: %w", err)
}
tracerProvider, err := initTracing(ctx, cfgSet.Tracing, res)
if err != nil {
return RouterBundle{}, fmt.Errorf("init tracing: %w", err)
}
routing := cfgSet.Routing
router := initMinimalRouter(len(routing.Flows), metrics, log)
router.rateLimiter, err = initRateLimiter(routing.RateLimiter)
if err != nil {
return RouterBundle{}, fmt.Errorf("init rate limiter: %w", err)
}
trustedProxies, err := parseTrustedProxies(cfgSet.Routing.TrustedProxies)
if err != nil {
return RouterBundle{}, fmt.Errorf("parse trusted proxies: %w", err)
}
router.trustedProxies = trustedProxies
tlsRegistry := tlsutil.NewRegistry()
for _, fcfg := range routing.Flows {
compiledFlow, compileErr := compileFlow(fcfg, trustedProxies, metrics, tlsRegistry, log)
if compileErr != nil {
return RouterBundle{}, fmt.Errorf("compile flow %q: %w", fcfg.Path, compileErr)
}
router.flows = append(router.flows, compiledFlow)
}
router.registerFlows()
return RouterBundle{
Router: router,
TLSRegistry: tlsRegistry,
MeterProvider: meterProvider,
PromRegistry: promRegistry,
TracerProvider: tracerProvider,
}, nil
}
func (r *Router) registerFlows() {
r.chiRouter.NotFound(func(w http.ResponseWriter, req *http.Request) {
r.metrics.IncFailedRequestsTotal(metric.FailReasonNoMatchedFlow)
r.log.Error("no flow matched", zap.String("request_uri", req.URL.RequestURI()))
http.NotFound(w, req)
})
for i := range r.flows {
f := &r.flows[i]
middlewares := make([]func(http.Handler) http.Handler, 0, len(f.middlewares))
for _, m := range f.middlewares {
middlewares = append(middlewares, m.Handler)
}
r.chiRouter.With(middlewares...).Method(
f.method,
f.path,
r.newFlowHandler(f),
)
}
}
func initMinimalRouter(routesCount int, metrics *metric.Metrics, log *zap.Logger) *Router {
return &Router{
chiRouter: chi.NewMux(),
scatter: &defaultScatter{
log: log.Named("scatter"),
metrics: metrics,
},
aggregator: &defaultAggregator{},
flows: make([]flow, 0, routesCount),
log: log,
metrics: metrics,
rateLimiter: nil,
}
}
func initMetrics(ctx context.Context, cfg MetricsConfig, res *resource.Resource) (otelcommon.Provider, *prometheus.Registry, error) {
if !cfg.Enabled {
return otelcommon.NewNopProvider(), nil, nil
}
switch cfg.Exporter {
case "prometheus":
provider, reg, err := metric.NewOtelPrometheus(res)
if err != nil {
return nil, nil, fmt.Errorf("init prometheus metrics: %w", err)
}
return provider, reg, nil
case "otlp":
provider, err := metric.NewOtelOTLP(ctx, cfg.OTLP.Endpoint, cfg.OTLP.Insecure, cfg.OTLP.Interval, res)
if err != nil {
return nil, nil, fmt.Errorf("init otlp metrics: %w", err)
}
return provider, nil, nil
default:
return nil, nil, fmt.Errorf("unsupported metrics exporter: %q", cfg.Exporter)
}
}
func initTracing(ctx context.Context, cfg TracingConfig, res *resource.Resource) (otelcommon.Provider, error) {
if !cfg.Enabled {
return otelcommon.NewNopProvider(), nil
}
switch cfg.Exporter {
case "otlp":
tracerProvider, err := tracing.NewOtelOTLP(
ctx,
cfg.OTLP.Endpoint,
cfg.OTLP.Insecure,
cfg.OTLP.Interval,
cfg.SamplingRatio,
res,
)
if err != nil {
return nil, fmt.Errorf("init otlp tracing: %w", err)
}
return tracerProvider, nil
default:
return nil, fmt.Errorf("unsupported tracing exporter: %q", cfg.Exporter)
}
}
func initRateLimiter(cfg RateLimiterConfig) (*ratelimit.RateLimit, error) {
if !cfg.Enabled {
return nil, nil //nolint:nilnil // it is ok for optional modules
}
rl := ratelimit.New(cfg.Config)
if err := rl.Start(); err != nil {
return nil, fmt.Errorf("start rate limiter: %w", err)
}
return rl, nil
}
func parseTrustedProxies(proxies []string) ([]*net.IPNet, error) {
result := make([]*net.IPNet, 0, len(proxies))
for _, proxy := range proxies {
_, ipnet, err := net.ParseCIDR(proxy)
if err != nil {
return nil, fmt.Errorf("parse trusted proxy CIDR: %w", err)
}
result = append(result, ipnet)
}
return result, nil
}
func compileFlow(cfg FlowConfig, trustedProxies []*net.IPNet, metrics *metric.Metrics, tlsRegistry *tlsutil.Registry, log *zap.Logger) (flow, error) {
upstreams, err := initUpstreams(cfg.Upstreams, trustedProxies, metrics, tlsRegistry, log)
if err != nil {
return flow{}, fmt.Errorf("init upstreams: %w", err)
}
if cfg.Streaming && len(upstreams) != 1 {
return flow{}, fmt.Errorf(
"streaming flow '%s' must have exactly one upstream, got %d",
cfg.Path, len(upstreams),
)
}
// Aggregation only applies when the aggregator is actually reached: a flow
// with exactly one upstream is proxied directly (see Router.dispatch) and
// never sees it, streaming or not.
var aggregationParams aggregation
if !cfg.Streaming && len(upstreams) > 1 {
var initErr error
aggregationParams, initErr = initAggregation(*cfg.Aggregation, upstreams)
if initErr != nil {
return flow{}, fmt.Errorf("init aggregation: %w", initErr)
}
}
plugins, err := initPlugins(cfg.Plugins, log)
if err != nil {
return flow{}, fmt.Errorf("init plugins: %w", err)
}
middlewares, err := initMiddlewares(cfg.Middlewares, log)
if err != nil {
return flow{}, fmt.Errorf("init middlewares: %w", err)
}
return flow{
path: cfg.Path,
method: cfg.Method,
aggregation: aggregationParams,
upstreams: upstreams,
plugins: plugins,
middlewares: middlewares,
streaming: cfg.Streaming,
}, nil
}
func initAggregation(cfg AggregationConfig, upstreams []upstream) (aggregation, error) {
strategy, err := compileStrategy(cfg.Strategy)
if err != nil {
return aggregation{}, err
}
agg := aggregation{
bestEffort: cfg.BestEffort,
strategy: strategy,
conflictPolicy: conflictPolicyOverwrite, // default, value used only for merge strategy
preferredUpstream: -1, // default, value used only for merge strategy
}
if strategy != strategyMerge {
return agg, nil
}
conflict, err := compileConflictPolicy(cfg.OnConflict.Policy)
if err != nil {
return aggregation{}, err
}
agg.conflictPolicy = conflict
if conflict == conflictPolicyPrefer {
if cfg.OnConflict.Upstream == "" {
return aggregation{}, errors.New("no upstream specified for on_conflict prefer policy")
}
idx, found := searchUpstream(cfg.OnConflict.Upstream, upstreams)
if !found {
return aggregation{}, errors.New("preferred upstream for on_conflict policy does not exist")
}
agg.preferredUpstream = idx
}
return agg, nil
}
func searchUpstream(name string, upstreams []upstream) (int, bool) {
for i, u := range upstreams {
if u.name() == name {
return i, true
}
}
return -1, false
}
func compileStrategy(s string) (aggregationStrategy, error) {
switch s {
case "array":
return strategyArray, nil
case "merge":
return strategyMerge, nil
case "namespace":
return strategyNamespace, nil
default:
return 0, fmt.Errorf("unknown aggregation strategy: %q", s)
}
}
func compileConflictPolicy(p string) (conflictPolicy, error) {
switch p {
case "overwrite":
return conflictPolicyOverwrite, nil
case "error":
return conflictPolicyError, nil
case "first":
return conflictPolicyFirst, nil
case "prefer":
return conflictPolicyPrefer, nil
default:
return 0, fmt.Errorf("unknown aggregation conflict policy: %q", p)
}
}
func initUpstreams(cfgs []UpstreamConfig, trustedProxies []*net.IPNet, metrics *metric.Metrics, tlsRegistry *tlsutil.Registry, log *zap.Logger) ([]upstream, error) {
upstreams := make([]upstream, 0, len(cfgs))
for _, cfg := range cfgs {
u, err := buildUpstream(cfg, trustedProxies, metrics, tlsRegistry, log)
if err != nil {
return nil, fmt.Errorf("build upstream: %w", err)
}
upstreams = append(upstreams, u)
}
return upstreams, nil
}
func buildUpstream(cfg UpstreamConfig, trustedProxies []*net.IPNet, metrics *metric.Metrics, tlsRegistry *tlsutil.Registry, log *zap.Logger) (upstream, error) {
tlsCfg, err := buildUpstreamTLSConfig(cfg.TLS, cfg.Transport.HTTP2, tlsRegistry)
if err != nil {
return nil, fmt.Errorf("build TLS config: %w", err)
}
transport := buildUpstreamTransport(cfg, tlsCfg)
if cfg.TLS.InsecureSkipVerify {
log.Warn("TLS verification disabled for upstream",
zap.String("upstream", cfg.Name),
zap.Strings("hosts", cfg.Hosts),
)
}
client := &http.Client{Transport: transport, Timeout: cfg.Timeout}
streamClient := &http.Client{Transport: transport}
if !cfg.Policy.FollowRedirects {
client.CheckRedirect = noRedirect
streamClient.CheckRedirect = noRedirect
}
return &httpUpstream{
cfg: buildUpstreamConfig(cfg, trustedProxies),
state: buildUpstreamState(cfg.Hosts),
circuitBreaker: buildCircuitBreaker(cfg.Policy.CircuitBreakerConfig),
metrics: metrics,
log: log,
client: client,
streamClient: streamClient,
}, nil
}
func noRedirect(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
}
func buildUpstreamConfig(cfg UpstreamConfig, trustedProxies []*net.IPNet) upstreamConfig {
name := cfg.Name
if name == "" {
name = makeUpstreamName(cfg.Method, cfg.Hosts)
}
return upstreamConfig{
id: uuid.NewString(),
name: name,
hosts: cfg.Hosts,
path: cfg.Path,
method: cfg.Method,
timeout: cfg.Timeout,
forwardHeaders: cfg.ForwardHeaders,
forwardQueries: cfg.ForwardQueries,
forwardParams: cfg.ForwardParams,
trustedProxies: trustedProxies,
lbMode: lbMode(cfg.Policy.LoadBalancingConfig.Mode),
policy: buildUpstreamPolicy(cfg.Policy),
}
}
func buildUpstreamState(hosts []string) upstreamState {
return upstreamState{
currentHostIdx: 0,
activeConnections: make([]int64, len(hosts)),
}
}
func buildUpstreamPolicy(cfg PolicyConfig) upstreamPolicy {
// Canonicalized so a config entry matches regardless of how it's cased:
// filterHeaders looks these up against http.Header, which net/http
// always stores in net/textproto canonical form.
headerBlacklist := make(map[string]struct{}, len(cfg.HeaderBlacklist))
for _, h := range cfg.HeaderBlacklist {
headerBlacklist[textproto.CanonicalMIMEHeaderKey(h)] = struct{}{}
}
return upstreamPolicy{
headerBlacklist: headerBlacklist,
requireBody: cfg.RequireBody,
maxResponseBodySize: cfg.MaxResponseBodySize,
retry: retryPolicy{
maxRetries: cfg.RetryConfig.MaxRetries,
retryOnStatuses: cfg.RetryConfig.RetryOnStatuses,
backoffDelay: cfg.RetryConfig.BackoffDelay,
},
}
}
func buildCircuitBreaker(cfg CircuitBreakerConfig) *circuitbreaker.CircuitBreaker {
if !cfg.Enabled {
return nil
}
return circuitbreaker.New(cfg.MaxFailures, cfg.ResetTimeout)
}
func buildUpstreamTransport(cfg UpstreamConfig, tlsCfg *tls.Config) *http.Transport {
t := &http.Transport{
MaxIdleConns: cfg.Transport.MaxIdleConns,
MaxIdleConnsPerHost: cfg.Transport.MaxIdleConnsPerHost,
IdleConnTimeout: cfg.Transport.IdleConnTimeout,
TLSClientConfig: tlsCfg,
}
if cfg.Transport.HTTP2 == "off" {
protocols := new(http.Protocols)
protocols.SetHTTP1(true)
t.Protocols = protocols
return t
}
t.ForceAttemptHTTP2 = true
if tlsCfg != nil {
protocols := new(http.Protocols)
protocols.SetHTTP1(true)
protocols.SetHTTP2(true)
t.Protocols = protocols
}
return t
}
func buildUpstreamTLSConfig(cfg TLSConfig, http2 string, reg *tlsutil.Registry) (*tls.Config, error) {
if !cfg.Enabled {
return nil, nil //nolint:nilnil // its ok here
}
minVer, err := tlsutil.ParseVersion(cfg.MinVersion)
if err != nil {
return nil, err
}
reloader, err := tlsutil.NewReloader(tlsutil.ReloaderConfig{
CertFile: cfg.CertFile,
CAFile: cfg.CAFile,
KeyFile: cfg.KeyFile,
ServerName: cfg.ServerName,
MinVersion: minVer,
InsecureSkipVerify: cfg.InsecureSkipVerify,
DisableHTTP2: http2 == "off",
})
if err != nil {
return nil, err
}
reg.Register(reloader)
return reloader.ClientConfig(), nil
}
// makeUpstreamName returns the upstream name made up of its method and hosts separated by a hyphen.
func makeUpstreamName(method string, hosts []string) string {
sb := strings.Builder{}
sb.WriteString(strings.ToLower(method))
sb.WriteString("-")
for i, host := range hosts {
sb.WriteString(strings.ToLower(host))
if i != len(hosts)-1 {
sb.WriteString("-")
}
}
return sb.String()
}