diff --git a/config/parsing/parse.go b/config/parsing/parse.go index 11a0eab1..f6db7fe7 100644 --- a/config/parsing/parse.go +++ b/config/parsing/parse.go @@ -92,4 +92,8 @@ const ( // MDKeyDialTimeout sets the dial timeout for outbound connections. MDKeyDialTimeout = "dialTimeout" + + // MDKeyLabels holds static key/value labels attached to a service's + // records and logs. + MDKeyLabels = "labels" ) diff --git a/config/parsing/service/parse.go b/config/parsing/service/parse.go index c58a438e..f6dd87ed 100644 --- a/config/parsing/service/parse.go +++ b/config/parsing/service/parse.go @@ -115,6 +115,7 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) { var observerPeriod time.Duration var netnsIn, netnsOut string var dialTimeout time.Duration + var labels map[string]string var limiterRefreshInterval time.Duration var limiterCleanupInterval time.Duration @@ -150,6 +151,14 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) { limiterRefreshInterval = mdutil.GetDuration(md, parsing.MDKeyLimiterRefreshInterval) limiterCleanupInterval = mdutil.GetDuration(md, parsing.MDKeyLimiterCleanupInterval) limiterScope = mdutil.GetString(md, parsing.MDKeyLimiterScope) + + labels = mdutil.GetStringMapString(md, parsing.MDKeyLabels) + } + + if len(labels) > 0 { + serviceLogger = serviceLogger.WithFields(map[string]any{ + "labels": labels, + }) } listenerLogger := serviceLogger.WithFields(map[string]any{ @@ -349,6 +358,7 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) { xservice.ObserverOption(registry.ObserverRegistry().Get(cfg.Observer)), xservice.ObserverPeriodOption(observerPeriod), xservice.LoggerOption(serviceLogger), + xservice.LabelsOption(labels), ) serviceLogger.Infof("listening on %s/%s", s.Addr().String(), s.Addr().Network()) diff --git a/ctx/value.go b/ctx/value.go index e6be320d..98c7edbb 100644 --- a/ctx/value.go +++ b/ctx/value.go @@ -100,3 +100,17 @@ func ClientIDFromContext(ctx context.Context) ClientID { v, _ := ctx.Value(clientIDKey{}).(ClientID) return v } + +// labelsKey saves the static service labels. +type labelsKey struct{} + +// ContextWithLabels returns a new context carrying the given service labels. +func ContextWithLabels(ctx context.Context, labels map[string]string) context.Context { + return context.WithValue(ctx, labelsKey{}, labels) +} + +// LabelsFromContext returns the service labels stored in the context, or nil. +func LabelsFromContext(ctx context.Context) map[string]string { + v, _ := ctx.Value(labelsKey{}).(map[string]string) + return v +} diff --git a/ctx/value_test.go b/ctx/value_test.go index be4312c1..a0166a8e 100644 --- a/ctx/value_test.go +++ b/ctx/value_test.go @@ -3,6 +3,7 @@ package ctx import ( "context" "net" + "reflect" "testing" ) @@ -149,6 +150,29 @@ func TestClientIDFromContext_WrongType(t *testing.T) { } } +func TestContextWithLabels(t *testing.T) { + labels := map[string]string{"tenant": "acme", "region": "eu"} + ctx := ContextWithLabels(context.Background(), labels) + + got := LabelsFromContext(ctx) + if !reflect.DeepEqual(got, labels) { + t.Errorf("LabelsFromContext() = %v, want %v", got, labels) + } +} + +func TestLabelsFromContext_Empty(t *testing.T) { + if got := LabelsFromContext(context.Background()); got != nil { + t.Errorf("LabelsFromContext(empty) = %v, want nil", got) + } +} + +func TestLabelsFromContext_WrongType(t *testing.T) { + ctx := context.WithValue(context.Background(), labelsKey{}, "not-a-map") + if got := LabelsFromContext(ctx); got != nil { + t.Errorf("LabelsFromContext(wrong type) = %v, want nil", got) + } +} + func TestMultipleValuesInContext(t *testing.T) { ctx := context.Background() ctx = ContextWithSrcAddr(ctx, &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 8080}) diff --git a/recorder/recorder.go b/recorder/recorder.go index c2eb7916..cfce680d 100644 --- a/recorder/recorder.go +++ b/recorder/recorder.go @@ -7,6 +7,7 @@ import ( "time" "github.com/go-gost/core/recorder" + xctx "github.com/go-gost/x/ctx" ) const ( @@ -82,16 +83,17 @@ type DNSRecorderObject struct { // level — addresses, protocols, transferred bytes, and optional sub-records // for HTTP, WebSocket, TLS, and DNS traffic. type HandlerRecorderObject struct { - Node string `json:"node,omitempty"` - Service string `json:"service"` - Network string `json:"network"` - RemoteAddr string `json:"remote"` - LocalAddr string `json:"local"` - ClientAddr string `json:"client"` - SrcAddr string `json:"src"` - DstAddr string `json:"dst"` - Host string `json:"host"` - Proto string `json:"proto,omitempty"` + Node string `json:"node,omitempty"` + Service string `json:"service"` + Labels map[string]string `json:"labels,omitempty"` + Network string `json:"network"` + RemoteAddr string `json:"remote"` + LocalAddr string `json:"local"` + ClientAddr string `json:"client"` + SrcAddr string `json:"src"` + DstAddr string `json:"dst"` + Host string `json:"host"` + Proto string `json:"proto,omitempty"` ClientIP string `json:"clientIP"` ClientID string `json:"clientID,omitempty"` HTTP *HTTPRecorderObject `json:"http,omitempty"` @@ -115,6 +117,10 @@ func (p *HandlerRecorderObject) Record(ctx context.Context, r recorder.Recorder) return nil } + if p.Labels == nil { + p.Labels = xctx.LabelsFromContext(ctx) + } + data, err := json.Marshal(p) if err != nil { return err diff --git a/service/service.go b/service/service.go index 5decaa38..6d1b79e0 100644 --- a/service/service.go +++ b/service/service.go @@ -43,6 +43,7 @@ type options struct { observer observer.Observer observerPeriod time.Duration logger logger.Logger + labels map[string]string } // Option is a functional option for configuring a service. @@ -113,6 +114,14 @@ func ObserverPeriodOption(period time.Duration) Option { } } +// LabelsOption sets the static labels attached to the service's records +// and logs. +func LabelsOption(labels map[string]string) Option { + return func(opts *options) { + opts.labels = labels + } +} + // LoggerOption sets the logger for the service. If not provided, a no-op // logger is used. func LoggerOption(logger logger.Logger) Option { @@ -245,6 +254,10 @@ func (s *defaultService) Serve() error { sid := xid.New().String() ctx = xctx.ContextWithSid(ctx, xctx.Sid(sid)) + if len(s.options.labels) > 0 { + ctx = xctx.ContextWithLabels(ctx, s.options.labels) + } + log := s.options.logger.WithFields(map[string]any{ "sid": sid, })