mirror of
https://github.com/hanzoai/gateway.git
synced 2026-10-30 15:04:44 +00:00
* feat(middleware): own JWT validation + identity-strip + trust helper Moves Auth + StripIdentityHeaders from zip/middleware to gateway/middleware. Adds gateway/trust.go exporting AssertGatewayMinted for downstream subsystems to verify the request flowed through gateway. The unified cloud binary's pipeline now reads: zip generic middleware (Recover, Logger, RequestID, Telemetry, ...) gateway.middleware.StripIdentityHeaders gateway.middleware.Auth(verifier) ... downstream subsystem handlers ... * feat(gateway): HIP-0110 ZAP wire envelopes + BuildApp entry * feat(gateway): cmd/gateway HIP-0110 edge-process main * refactor(gateway): tighten HIP-0110 surface — 588 LOC across 4 files * build(gateway): fix module pins, MsgType uint16 overflow, drop legacy jaeger - go.mod: hanzoai/cloud v0.0.0 → v0.1.0, hanzoai/zip v0.0.0 → v0.1.1 (zip v0.1.1 carries the json/v2-at-edge work merged in zip#3; cloud v0.1.0 carries the types-leaf + clients refactor). - zap_wire.go: MsgTypeForward/Push/Subscribe were 0x1010/0x1020/0x1030 (uint16) but zap.FinishWithFlags expects the type in the upper byte (type << 8). Anything > 0xFF overflows uint16. Repacked into 0x80/0x81/0x82 — still upper-half of the byte (no collision with base's 100-103 IDs). - executor.go: dropped krakend-opencensus/v2/exporter/jaeger blank import. The exporter's Thrift API is incompatible with current jaeger-client-go; inter-service tracing is migrating to OTel (kotel) + ZAP anyway. - cmd/gateway/main.go: c.SendString → c.Fiber().SendString (zip.Ctx exposes Send via the underlying fiber.Ctx; matches the pattern used for binary responses). Verified: go build -mod=mod ./... clean. Binary at /tmp/gateway = 108 MiB. TestStrip+TestAuth+TestSecure all pass. * test(gateway): per-conn memory profile — 8 KiB/conn, 1:1 goroutine Raw-TCP-dial loadgen (not http.Client, which spawns 3 client-side goroutines that contaminate the gateway-side measurement). Spins up zip.App on a local listener, dials N raw TCP conns each holding /hold, samples runtime.MemStats.HeapAlloc delta. Apple M1 Max, Go 1.26, zip on Fiber v3 v3.2.0: 1,000 conns: 8.05 KiB/conn, 1.00 goroutine/conn, 14 MiB heap 8,454 conns: 8.02 KiB/conn, 1.00 goroutine/conn, 72 MiB heap (linear) Discipline: fasthttp gives us 1 goroutine per accepted conn already. The HIP-0110 architecture preserves that — ZAP-forward to cloud is sync from the gateway goroutine, no fan-out. Reverse-push lookups are O(1) on a per-replica sync.Map, no extra goroutines. Extrapolation: 10k conns → ~80 MiB / replica 100k conns → ~800 MiB (1 GiB pod ceiling) 10 replicas behind ingress → 1M concurrent client conns Cloud side has zero per-conn cost — worker pool dispatches N ZAP requests per pod. This is why a stateless gateway LB in front of cloud maximizes compute efficiency: 10-100× fewer cloud replicas than gateway replicas required for the same client population. * deps: bump zip v0.1.1 → v0.2.0; pin cloud at resolvable pseudo-version cloud cannot be tagged yet due to upstream GOPROXY blockers (kms repo >500MB, vfs missing root package at v0.3.1). Pin cloud at the pseudo-version derived from main HEAD so consumers can resolve via proxy.golang.org without local replace dirs.
444 lines
16 KiB
Go
444 lines
16 KiB
Go
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
krakendbf "github.com/krakend/bloomfilter/v2/krakend"
|
|
asyncamqp "github.com/krakend/krakend-amqp/v2/async"
|
|
cel "github.com/krakend/krakend-cel/v2"
|
|
cmd "github.com/krakend/krakend-cobra/v2"
|
|
_ "github.com/krakend/krakend-cors/v2/gin" // keep dep, CORS handled by hostProxyMiddleware
|
|
gelf "github.com/krakend/krakend-gelf/v2"
|
|
gologging "github.com/krakend/krakend-gologging/v2"
|
|
influxdb "github.com/krakend/krakend-influx/v2"
|
|
jose "github.com/krakend/krakend-jose/v2"
|
|
logstash "github.com/krakend/krakend-logstash/v2"
|
|
metrics "github.com/krakend/krakend-metrics/v2/gin"
|
|
opencensus "github.com/krakend/krakend-opencensus/v2"
|
|
_ "github.com/krakend/krakend-opencensus/v2/exporter/datadog"
|
|
_ "github.com/krakend/krakend-opencensus/v2/exporter/influxdb"
|
|
// jaeger exporter dropped: upstream uses an opencensus/Thrift API combo
|
|
// incompatible with current jaeger-client-go. Migration path is OTel
|
|
// (kotel below) + ZAP for inter-service.
|
|
_ "github.com/krakend/krakend-opencensus/v2/exporter/ocagent"
|
|
_ "github.com/krakend/krakend-opencensus/v2/exporter/prometheus"
|
|
_ "github.com/krakend/krakend-opencensus/v2/exporter/stackdriver"
|
|
_ "github.com/krakend/krakend-opencensus/v2/exporter/xray"
|
|
_ "github.com/krakend/krakend-opencensus/v2/exporter/zipkin"
|
|
kotel "github.com/krakend/krakend-otel"
|
|
otellura "github.com/krakend/krakend-otel/lura"
|
|
otelgin "github.com/krakend/krakend-otel/router/gin"
|
|
pubsub "github.com/krakend/krakend-pubsub/v2"
|
|
"github.com/luraproject/lura/v2/async"
|
|
"github.com/luraproject/lura/v2/config"
|
|
"github.com/luraproject/lura/v2/core"
|
|
"github.com/luraproject/lura/v2/logging"
|
|
"github.com/luraproject/lura/v2/proxy"
|
|
router "github.com/luraproject/lura/v2/router/gin"
|
|
"github.com/luraproject/lura/v2/sd/dnssrv"
|
|
serverhttp "github.com/luraproject/lura/v2/transport/http/server"
|
|
server "github.com/luraproject/lura/v2/transport/http/server/plugin"
|
|
)
|
|
|
|
// NewExecutor returns an executor for the cmd package. The executor initalizes the entire gateway by
|
|
// registering the components and composing a RouterFactory wrapping all the middlewares.
|
|
func NewExecutor(ctx context.Context) cmd.Executor {
|
|
eb := new(ExecutorBuilder)
|
|
return eb.NewCmdExecutor(ctx)
|
|
}
|
|
|
|
// PluginLoader defines the interface for the collaborator responsible of starting the plugin loaders
|
|
// Deprecated: Use PluginLoaderWithContext
|
|
type PluginLoader interface {
|
|
Load(folder, pattern string, logger logging.Logger)
|
|
}
|
|
|
|
// PluginLoaderWithContext defines the interface for the collaborator responsible of starting the plugin loaders
|
|
type PluginLoaderWithContext interface {
|
|
LoadWithContext(ctx context.Context, folder, pattern string, logger logging.Logger)
|
|
}
|
|
|
|
// SubscriberFactoriesRegister registers all the required subscriber factories from the available service
|
|
// discover components and adapters and returns a service register function.
|
|
// The service register function will register the service by the given name and port to all the available
|
|
// service discover clients
|
|
type SubscriberFactoriesRegister interface {
|
|
Register(context.Context, config.ServiceConfig, logging.Logger) func(string, int)
|
|
}
|
|
|
|
// TokenRejecterFactory returns a jose.ChainedRejecterFactory containing all the required jose.RejecterFactory.
|
|
// It also should setup and manage any service related to the management of the revocation process, if required.
|
|
type TokenRejecterFactory interface {
|
|
NewTokenRejecter(context.Context, config.ServiceConfig, logging.Logger, func(string, int)) (jose.ChainedRejecterFactory, error)
|
|
}
|
|
|
|
// MetricsAndTracesRegister registers the defined observability components and returns a metrics collector,
|
|
// if required.
|
|
type MetricsAndTracesRegister interface {
|
|
Register(context.Context, config.ServiceConfig, logging.Logger) *metrics.Metrics
|
|
}
|
|
|
|
// EngineFactory returns a gin engine, ready to be passed to the Gateway RouterFactory
|
|
type EngineFactory interface {
|
|
NewEngine(config.ServiceConfig, router.EngineOptions) *gin.Engine
|
|
}
|
|
|
|
// ProxyFactory returns a Gateway proxy factory, ready to be passed to the Gateway RouterFactory
|
|
type ProxyFactory interface {
|
|
NewProxyFactory(logging.Logger, proxy.BackendFactory, *metrics.Metrics) proxy.Factory
|
|
}
|
|
|
|
// BackendFactory returns a Gateway backend factory, ready to be passed to the Gateway proxy factory
|
|
type BackendFactory interface {
|
|
NewBackendFactory(context.Context, logging.Logger, *metrics.Metrics) proxy.BackendFactory
|
|
}
|
|
|
|
// HandlerFactory returns a Gateway router handler factory, ready to be passed to the Gateway RouterFactory
|
|
type HandlerFactory interface {
|
|
NewHandlerFactory(logging.Logger, *metrics.Metrics, jose.RejecterFactory) router.HandlerFactory
|
|
}
|
|
|
|
// LoggerFactory returns a Gateway Logger factory, ready to be passed to the Gateway RouterFactory
|
|
type LoggerFactory interface {
|
|
NewLogger(config.ServiceConfig) (logging.Logger, io.Writer, error)
|
|
}
|
|
|
|
// RunServer defines the interface of a function used by the Gateway router to start the service
|
|
type RunServer func(context.Context, config.ServiceConfig, http.Handler) error
|
|
|
|
// RunServerFactory returns a RunServer with several wraps around the injected one
|
|
type RunServerFactory interface {
|
|
NewRunServer(logging.Logger, router.RunServerFunc) RunServer
|
|
}
|
|
|
|
// AgentStarter defines a type that starts a set of agents
|
|
type AgentStarter interface {
|
|
Start(
|
|
context.Context,
|
|
[]*config.AsyncAgent,
|
|
logging.Logger,
|
|
chan<- string,
|
|
proxy.Factory,
|
|
) func() error
|
|
}
|
|
|
|
// ExecutorBuilder is a composable builder. Every injected property is used by the NewCmdExecutor method.
|
|
type ExecutorBuilder struct {
|
|
// PluginLoader is deprecated: Use PluginLoaderWithContext
|
|
PluginLoader PluginLoader
|
|
PluginLoaderWithContext PluginLoaderWithContext
|
|
LoggerFactory LoggerFactory
|
|
SubscriberFactoriesRegister SubscriberFactoriesRegister
|
|
TokenRejecterFactory TokenRejecterFactory
|
|
MetricsAndTracesRegister MetricsAndTracesRegister
|
|
EngineFactory EngineFactory
|
|
|
|
ProxyFactory ProxyFactory
|
|
BackendFactory BackendFactory
|
|
HandlerFactory HandlerFactory
|
|
RunServerFactory RunServerFactory
|
|
AgentStarterFactory AgentStarter
|
|
|
|
Middlewares []gin.HandlerFunc
|
|
}
|
|
|
|
// NewCmdExecutor returns an executor for the cmd package. The executor initializes the entire gateway by
|
|
// delegating most of the tasks to the injected collaborators. They register the components and
|
|
// compose a RouterFactory wrapping all the middlewares.
|
|
// Every nil collaborator is replaced by the default one offered by this package.
|
|
func (e *ExecutorBuilder) NewCmdExecutor(ctx context.Context) cmd.Executor {
|
|
e.checkCollaborators()
|
|
|
|
return func(cfg config.ServiceConfig) {
|
|
cfg.Normalize()
|
|
|
|
logger, gelfWriter, gelfErr := e.LoggerFactory.NewLogger(cfg)
|
|
if gelfErr != nil {
|
|
return
|
|
}
|
|
|
|
logger.Info(fmt.Sprintf("Starting Gateway v%s", core.KrakendVersion))
|
|
startReporter(ctx, logger, cfg)
|
|
|
|
if wd, err := os.Getwd(); err == nil {
|
|
logger.Info("Working directory is", wd)
|
|
}
|
|
|
|
dnssrv.SetTTL(cfg.DNSCacheTTL)
|
|
|
|
if cfg.Plugin != nil {
|
|
e.PluginLoaderWithContext.LoadWithContext(ctx, cfg.Plugin.Folder, cfg.Plugin.Pattern, logger)
|
|
}
|
|
|
|
metricCollector := e.MetricsAndTracesRegister.Register(ctx, cfg, logger)
|
|
if metricsAndTracesCloser, ok := e.MetricsAndTracesRegister.(io.Closer); ok {
|
|
defer metricsAndTracesCloser.Close()
|
|
}
|
|
|
|
// Initializes the global cache for the JWK clients if enabled in the config
|
|
if err := jose.SetGlobalCacher(logger, cfg.ExtraConfig); err != nil && err != jose.ErrNoValidatorCfg {
|
|
logger.Error("[SERVICE: JOSE]", err.Error())
|
|
}
|
|
tokenRejecterFactory, err := e.TokenRejecterFactory.NewTokenRejecter(
|
|
ctx,
|
|
cfg,
|
|
logger,
|
|
e.SubscriberFactoriesRegister.Register(ctx, cfg, logger),
|
|
)
|
|
if err != nil && err != krakendbf.ErrNoConfig {
|
|
logger.Warning("[SERVICE: Bloomfilter]", err.Error())
|
|
}
|
|
|
|
bpf := e.BackendFactory.NewBackendFactory(ctx, logger, metricCollector)
|
|
pf := e.ProxyFactory.NewProxyFactory(logger, bpf, metricCollector)
|
|
// we move the proxy factory out of the default proxy factory to make
|
|
// sure that is always the outer middleware and that wraps any internal
|
|
// proxy layer middleware:
|
|
pf = otellura.ProxyFactory(pf)
|
|
|
|
agentPing := make(chan string, len(cfg.AsyncAgents))
|
|
|
|
handlerF := e.HandlerFactory.NewHandlerFactory(logger, metricCollector, tokenRejecterFactory)
|
|
handlerF = otelgin.New(handlerF)
|
|
|
|
runServerChain := serverhttp.RunServerWithLoggerFactory(logger)
|
|
runServerChain = otellura.GlobalRunServer(logger, runServerChain)
|
|
runServerChain = router.RunServerFunc(e.RunServerFactory.NewRunServer(logger, runServerChain))
|
|
|
|
// Start the inbound ZAP listener (TLS 1.3+PQ) for external clients.
|
|
InitZapListenerFromEnv()
|
|
defer StopZapListener()
|
|
// The ZAP-HTTP listener is booted lazily by the RunServer chain
|
|
// (DefaultRunServerFactory.NewRunServer) once the fully-wrapped
|
|
// handler is available. Just ensure we shut it down on exit.
|
|
defer stopZapHTTPListener()
|
|
|
|
// setup the gateway router
|
|
routerFactory := router.NewFactory(router.Config{
|
|
Engine: e.EngineFactory.NewEngine(cfg, router.EngineOptions{
|
|
Logger: logger,
|
|
Writer: gelfWriter,
|
|
Health: (<-chan string)(agentPing),
|
|
}),
|
|
ProxyFactory: pf,
|
|
Middlewares: e.Middlewares,
|
|
Logger: logger,
|
|
HandlerFactory: handlerF,
|
|
RunServer: runServerChain,
|
|
})
|
|
|
|
// start the engines
|
|
logger.Info("Starting the Gateway instance")
|
|
|
|
if len(cfg.AsyncAgents) == 0 {
|
|
routerFactory.NewWithContext(ctx).Run(cfg)
|
|
return
|
|
}
|
|
|
|
// start the async agents in the same error group as the router
|
|
g, gctx := errgroup.WithContext(ctx)
|
|
gctx, closeGroupCtx := context.WithCancel(gctx)
|
|
|
|
if cfg.SequentialStart {
|
|
waitAgents := e.AgentStarterFactory.Start(gctx, cfg.AsyncAgents, logger, (chan<- string)(agentPing), pf)
|
|
g.Go(waitAgents)
|
|
} else {
|
|
g.Go(func() error {
|
|
return e.AgentStarterFactory.Start(gctx, cfg.AsyncAgents, logger, (chan<- string)(agentPing), pf)()
|
|
})
|
|
}
|
|
|
|
g.Go(func() error {
|
|
logger.Info("[SERVICE: Gin] Building the router")
|
|
routerFactory.NewWithContext(ctx).Run(cfg)
|
|
closeGroupCtx()
|
|
return nil
|
|
})
|
|
|
|
g.Wait()
|
|
}
|
|
}
|
|
|
|
func (e *ExecutorBuilder) checkCollaborators() {
|
|
if e.PluginLoader == nil {
|
|
e.PluginLoader = new(pluginLoader)
|
|
}
|
|
if e.PluginLoaderWithContext == nil {
|
|
e.PluginLoaderWithContext = new(pluginLoader)
|
|
}
|
|
if e.SubscriberFactoriesRegister == nil {
|
|
e.SubscriberFactoriesRegister = new(registerSubscriberFactories)
|
|
}
|
|
if e.TokenRejecterFactory == nil {
|
|
e.TokenRejecterFactory = new(BloomFilterJWT)
|
|
}
|
|
if e.MetricsAndTracesRegister == nil {
|
|
e.MetricsAndTracesRegister = new(MetricsAndTraces)
|
|
}
|
|
if e.EngineFactory == nil {
|
|
e.EngineFactory = new(engineFactory)
|
|
}
|
|
if e.ProxyFactory == nil {
|
|
e.ProxyFactory = new(proxyFactory)
|
|
}
|
|
if e.BackendFactory == nil {
|
|
e.BackendFactory = new(backendFactory)
|
|
}
|
|
if e.HandlerFactory == nil {
|
|
e.HandlerFactory = new(handlerFactory)
|
|
}
|
|
if e.LoggerFactory == nil {
|
|
e.LoggerFactory = new(LoggerBuilder)
|
|
}
|
|
if e.RunServerFactory == nil {
|
|
e.RunServerFactory = new(DefaultRunServerFactory)
|
|
}
|
|
if e.AgentStarterFactory == nil {
|
|
e.AgentStarterFactory = async.AgentStarter([]async.Factory{asyncamqp.StartAgent})
|
|
}
|
|
}
|
|
|
|
// DefaultRunServerFactory creates the default RunServer by wrapping the injected RunServer
|
|
// with the plugin loader and the CORS module
|
|
type DefaultRunServerFactory struct{}
|
|
|
|
func (*DefaultRunServerFactory) NewRunServer(l logging.Logger, next router.RunServerFunc) RunServer {
|
|
// CORS is handled by hostProxyMiddleware in router_engine.go.
|
|
// Skip the upstream CORS wrapper to avoid duplicate headers.
|
|
httpRun := RunServer(server.New(
|
|
l,
|
|
server.RunServer(next),
|
|
))
|
|
// Boot the ZAP-HTTP listener once per process with the same fully
|
|
// composed handler that the HTTP listener serves. Additive: HTTP
|
|
// keeps serving regardless of ZAP-HTTP state.
|
|
return func(ctx context.Context, cfg config.ServiceConfig, handler http.Handler) error {
|
|
startZapHTTPListenerOnce(l, handler)
|
|
return httpRun(ctx, cfg, handler)
|
|
}
|
|
}
|
|
|
|
// LoggerBuilder is the default BuilderFactory implementation.
|
|
type LoggerBuilder struct{}
|
|
|
|
// NewLogger sets up the logging components as defined at the configuration.
|
|
func (LoggerBuilder) NewLogger(cfg config.ServiceConfig) (logging.Logger, io.Writer, error) {
|
|
var writers []io.Writer
|
|
gelfWriter, gelfErr := gelf.NewWriter(cfg.ExtraConfig)
|
|
if gelfErr == nil {
|
|
writers = append(writers, gelfWriterWrapper{gelfWriter})
|
|
gologging.SetFormatterSelector(func(w io.Writer) string {
|
|
switch w.(type) {
|
|
case gelfWriterWrapper:
|
|
return "%{message}"
|
|
default:
|
|
return gologging.DefaultPattern
|
|
}
|
|
})
|
|
} else {
|
|
gelfWriter = nil
|
|
}
|
|
|
|
logger, gologgingErr := logstash.NewLogger(cfg.ExtraConfig)
|
|
|
|
if gologgingErr != nil {
|
|
logger, gologgingErr = gologging.NewLogger(cfg.ExtraConfig, writers...)
|
|
|
|
if gologgingErr != nil {
|
|
var err error
|
|
logger, err = logging.NewLogger("DEBUG", os.Stdout, "GATEWAY")
|
|
if err != nil {
|
|
return logger, gelfWriter, err
|
|
}
|
|
if gologgingErr != gologging.ErrWrongConfig {
|
|
logger.Error("[SERVICE: Logging] Unable to create the logger:", gologgingErr.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
if gelfErr != nil && gelfErr != gelf.ErrWrongConfig {
|
|
logger.Error("[SERVICE: Logging][GELF] Unable to create the writer:", gelfErr.Error())
|
|
}
|
|
|
|
if gologgingErr == nil {
|
|
logger.Debug("[SERVICE: telemetry/logging] Improved logging started.")
|
|
}
|
|
|
|
return logger, gelfWriter, nil
|
|
}
|
|
|
|
// BloomFilterJWT is the default TokenRejecterFactory implementation.
|
|
type BloomFilterJWT struct{}
|
|
|
|
// NewTokenRejecter registers the bloomfilter component and links it to a token rejecter. Then it returns a chained
|
|
// rejecter factory with the created token rejecter and other based on the CEL component.
|
|
func (BloomFilterJWT) NewTokenRejecter(ctx context.Context, cfg config.ServiceConfig, l logging.Logger, reg func(n string, p int)) (jose.ChainedRejecterFactory, error) {
|
|
rejecter, err := krakendbf.Register(ctx, "krakend-bf", cfg, l, reg)
|
|
|
|
return jose.ChainedRejecterFactory([]jose.RejecterFactory{
|
|
jose.RejecterFactoryFunc(func(_ logging.Logger, _ *config.EndpointConfig) jose.Rejecter {
|
|
return jose.RejecterFunc(rejecter.RejectToken)
|
|
}),
|
|
jose.RejecterFactoryFunc(func(l logging.Logger, cfg *config.EndpointConfig) jose.Rejecter {
|
|
if r := cel.NewRejecter(l, cfg); r != nil {
|
|
return r
|
|
}
|
|
return jose.FixedRejecter(false)
|
|
}),
|
|
}), err
|
|
}
|
|
|
|
// MetricsAndTraces is the default implementation of the MetricsAndTracesRegister interface.
|
|
type MetricsAndTraces struct {
|
|
shutdownFn func()
|
|
}
|
|
|
|
// Register registers the metrics, influx and opencensus packages as required by the given configuration.
|
|
func (m *MetricsAndTraces) Register(ctx context.Context, cfg config.ServiceConfig, l logging.Logger) *metrics.Metrics {
|
|
metricCollector := metrics.New(ctx, cfg.ExtraConfig, l)
|
|
|
|
if err := influxdb.New(ctx, cfg.ExtraConfig, metricCollector, l); err != nil {
|
|
if err != influxdb.ErrNoConfig {
|
|
l.Warning("[SERVICE: InfluxDB]", err.Error())
|
|
}
|
|
} else {
|
|
l.Debug("[SERVICE: InfluxDB] Service correctly registered")
|
|
}
|
|
|
|
if err := opencensus.Register(ctx, cfg, append(opencensus.DefaultViews, pubsub.OpenCensusViews...)...); err != nil {
|
|
if err != opencensus.ErrNoConfig {
|
|
l.Warning("[SERVICE: OpenCensus]", err.Error())
|
|
}
|
|
} else {
|
|
l.Debug("[SERVICE: OpenCensus] Service correctly registered")
|
|
}
|
|
|
|
if shutdownFn, err := kotel.Register(ctx, l, cfg); err == nil {
|
|
m.shutdownFn = shutdownFn
|
|
} else {
|
|
l.Error(fmt.Sprintf("[SERVICE: OpenTelemetry] cannot register exporters: %s", err.Error()))
|
|
}
|
|
|
|
return metricCollector
|
|
}
|
|
|
|
func (m *MetricsAndTraces) Close() {
|
|
if m.shutdownFn != nil {
|
|
m.shutdownFn()
|
|
}
|
|
}
|
|
|
|
// startReporter is a no-op in the Hanzo fork — no telemetry is sent to upstream.
|
|
func startReporter(_ context.Context, _ logging.Logger, _ config.ServiceConfig) {}
|
|
|
|
type gelfWriterWrapper struct {
|
|
io.Writer
|
|
}
|