diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index 7176356..57761f2 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -719,6 +719,10 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, sort.Strings(s) return s } + // If metrics is enabled, server set this for all clients, so we can check only the first one. + // Ideally, we may have a field in response to indicate that query count should be shown, but + // it would break earlier version of ctrld, which only look list of clients in response. + withQueryCount := len(clients) > 0 && clients[0].IncludeQueryCount data := make([][]string, len(clients)) for i, c := range clients { row := []string{ @@ -727,10 +731,17 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, c.Mac, strings.Join(map2Slice(c.Source), ","), } + if withQueryCount { + row = append(row, strconv.FormatInt(c.QueryCount, 10)) + } data[i] = row } table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"IP", "Hostname", "Mac", "Discovered"}) + headers := []string{"IP", "Hostname", "Mac", "Discovered"} + if withQueryCount { + headers = append(headers, "Queries") + } + table.SetHeader(headers) table.SetAutoFormatHeaders(false) table.AppendBulk(data) table.Render() diff --git a/cmd/cli/control_server.go b/cmd/cli/control_server.go index 5ee7112..36749c1 100644 --- a/cmd/cli/control_server.go +++ b/cmd/cli/control_server.go @@ -10,6 +10,8 @@ import ( "sort" "time" + dto "github.com/prometheus/client_model/go" + "github.com/Control-D-Inc/ctrld" ) @@ -66,6 +68,25 @@ func (p *prog) registerControlServerHandler() { sort.Slice(clients, func(i, j int) bool { return clients[i].IP.Less(clients[j].IP) }) + if p.cfg.Service.MetricsQueryStats { + for _, client := range clients { + client.IncludeQueryCount = true + dm := &dto.Metric{} + m, err := statsClientQueriesCount.MetricVec.GetMetricWithLabelValues( + client.IP.String(), + client.Mac, + client.Hostname, + ) + if err != nil { + mainLog.Load().Debug().Err(err).Msgf("could not get metrics for client: %v", client) + continue + } + if err := m.Write(dm); err == nil { + client.QueryCount = int64(dm.Counter.GetValue()) + } + } + } + if err := json.NewEncoder(w).Encode(&clients); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/cmd/cli/dns_proxy.go b/cmd/cli/dns_proxy.go index fc319f2..0b6282e 100644 --- a/cmd/cli/dns_proxy.go +++ b/cmd/cli/dns_proxy.go @@ -54,6 +54,14 @@ type proxyRequest struct { ufr *upstreamForResult } +// proxyResponse contains data for proxying a DNS response from upstream. +type proxyResponse struct { + answer *dns.Msg + cached bool + clientInfo bool + upstream string +} + // upstreamForResult represents the result of processing rules for a request. type upstreamForResult struct { upstreams []string @@ -101,26 +109,49 @@ func (p *prog) serveDNS(listenerNum string) error { fmtSrcToDest := fmtRemoteToLocal(listenerNum, ci.Hostname, remoteAddr.String()) t := time.Now() ctrld.Log(ctx, mainLog.Load().Info(), "QUERY: %s: %s %s", fmtSrcToDest, dns.TypeToString[q.Qtype], domain) - res := p.upstreamFor(ctx, listenerNum, listenerConfig, remoteAddr, ci.Mac, domain) + ur := p.upstreamFor(ctx, listenerNum, listenerConfig, remoteAddr, ci.Mac, domain) + + labelValues := make([]string, 0, len(statsQueriesCountLabels)) + labelValues = append(labelValues, net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port))) + labelValues = append(labelValues, ci.IP) + labelValues = append(labelValues, ci.Mac) + labelValues = append(labelValues, ci.Hostname) + var answer *dns.Msg - if !res.matched && listenerConfig.Restricted { + if !ur.matched && listenerConfig.Restricted { ctrld.Log(ctx, mainLog.Load().Info(), "query refused, %s does not match any network policy", remoteAddr.String()) answer = new(dns.Msg) answer.SetRcode(m, dns.RcodeRefused) + labelValues = append(labelValues, "") // no upstream } else { var failoverRcode []int if listenerConfig.Policy != nil { failoverRcode = listenerConfig.Policy.FailoverRcodeNumbers } - answer = p.proxy(ctx, &proxyRequest{ + pr := p.proxy(ctx, &proxyRequest{ msg: m, ci: ci, failoverRcodes: failoverRcode, - ufr: res, + ufr: ur, }) + answer = pr.answer rtt := time.Since(t) ctrld.Log(ctx, mainLog.Load().Debug(), "received response of %d bytes in %s", answer.Len(), rtt) + upstream := pr.upstream + switch { + case pr.cached: + upstream = "cache" + case pr.clientInfo: + upstream = "client_info_table" + } + labelValues = append(labelValues, upstream) } + labelValues = append(labelValues, dns.TypeToString[q.Qtype]) + labelValues = append(labelValues, dns.RcodeToString[answer.Rcode]) + go func() { + p.WithLabelValuesInc(statsQueriesCount, labelValues...) + p.WithLabelValuesInc(statsClientQueriesCount, []string{ci.IP, ci.Mac, ci.Hostname}...) + }() if err := w.WriteMsg(answer); err != nil { ctrld.Log(ctx, mainLog.Load().Error().Err(err), "serveDNS: failed to send DNS response to client") } @@ -360,7 +391,7 @@ func (p *prog) proxyLanHostnameQuery(ctx context.Context, msg *dns.Msg) *dns.Msg return nil } -func (p *prog) proxy(ctx context.Context, req *proxyRequest) *dns.Msg { +func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse { var staleAnswer *dns.Msg upstreams := req.ufr.upstreams serveStaleCache := p.cache != nil && p.cfg.Service.CacheServeStale @@ -370,6 +401,8 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *dns.Msg { upstreams = []string{upstreamOS} } + res := &proxyResponse{} + // LAN/PTR lookup flow: // // 1. If there's matching rule, follow it. @@ -384,14 +417,18 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *dns.Msg { case isPrivatePtrLookup(req.msg): isLanOrPtrQuery = true if answer := p.proxyPrivatePtrLookup(ctx, req.msg); answer != nil { - return answer + res.answer = answer + res.clientInfo = true + return res } upstreams, upstreamConfigs = p.upstreamsAndUpstreamConfigForLanAndPtr(upstreams, upstreamConfigs) ctrld.Log(ctx, mainLog.Load().Debug(), "private PTR lookup, using upstreams: %v", upstreams) case isLanHostnameQuery(req.msg): isLanOrPtrQuery = true if answer := p.proxyLanHostnameQuery(ctx, req.msg); answer != nil { - return answer + res.answer = answer + res.clientInfo = true + return res } upstreams, upstreamConfigs = p.upstreamsAndUpstreamConfigForLanAndPtr(upstreams, upstreamConfigs) ctrld.Log(ctx, mainLog.Load().Debug(), "lan hostname lookup, using upstreams: %v", upstreams) @@ -413,7 +450,9 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *dns.Msg { if cachedValue.Expire.After(now) { ctrld.Log(ctx, mainLog.Load().Debug(), "hit cached response") setCachedAnswerTTL(answer, now, cachedValue.Expire) - return answer + res.answer = answer + res.cached = true + return res } staleAnswer = answer } @@ -475,7 +514,9 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *dns.Msg { ctrld.Log(ctx, mainLog.Load().Debug(), "serving stale cached response") now := time.Now() setCachedAnswerTTL(staleAnswer, now, now.Add(staleTTL)) - return staleAnswer + res.answer = staleAnswer + res.cached = true + return res } continue } @@ -509,12 +550,15 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *dns.Msg { hostname = req.ci.Hostname } ctrld.Log(ctx, mainLog.Load().Info(), "REPLY: %s -> %s (%s): %s", upstreams[n], req.ufr.srcAddr, hostname, dns.RcodeToString[answer.Rcode]) - return answer + res.answer = answer + res.upstream = upstreamConfig.Endpoint + return res } ctrld.Log(ctx, mainLog.Load().Error(), "all %v endpoints failed", upstreams) answer := new(dns.Msg) answer.SetRcode(req.msg, dns.RcodeServerFailure) - return answer + res.answer = answer + return res } func (p *prog) upstreamsAndUpstreamConfigForLanAndPtr(upstreams []string, upstreamConfigs []*ctrld.UpstreamConfig) ([]string, []*ctrld.UpstreamConfig) { diff --git a/cmd/cli/dns_proxy_test.go b/cmd/cli/dns_proxy_test.go index bd73d17..52d3edb 100644 --- a/cmd/cli/dns_proxy_test.go +++ b/cmd/cli/dns_proxy_test.go @@ -187,8 +187,8 @@ func TestCache(t *testing.T) { got1 := prog.proxy(context.Background(), req1) got2 := prog.proxy(context.Background(), req2) assert.NotSame(t, got1, got2) - assert.Equal(t, answer1.Rcode, got1.Rcode) - assert.Equal(t, answer2.Rcode, got2.Rcode) + assert.Equal(t, answer1.Rcode, got1.answer.Rcode) + assert.Equal(t, answer2.Rcode, got2.answer.Rcode) } func Test_ipAndMacFromMsg(t *testing.T) { diff --git a/cmd/cli/metrics.go b/cmd/cli/metrics.go new file mode 100644 index 0000000..ee64975 --- /dev/null +++ b/cmd/cli/metrics.go @@ -0,0 +1,150 @@ +package cli + +import ( + "context" + "encoding/json" + "net" + "net/http" + "runtime" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/prometheus/prom2json" +) + +// metricsServer represents a server to expose Prometheus metrics via HTTP. +type metricsServer struct { + server *http.Server + mux *http.ServeMux + reg *prometheus.Registry + addr string + started bool +} + +// newMetricsServer returns new metrics server. +func newMetricsServer(addr string, reg *prometheus.Registry) (*metricsServer, error) { + mux := http.NewServeMux() + ms := &metricsServer{ + server: &http.Server{Handler: mux}, + mux: mux, + reg: reg, + } + ms.addr = addr + ms.registerMetricsServerHandler() + return ms, nil +} + +// register adds handlers for given pattern. +func (ms *metricsServer) register(pattern string, handler http.Handler) { + ms.mux.Handle(pattern, handler) +} + +// registerMetricsServerHandler adds handlers for metrics server. +func (ms *metricsServer) registerMetricsServerHandler() { + ms.register("/metrics", promhttp.HandlerFor( + ms.reg, + promhttp.HandlerOpts{ + EnableOpenMetrics: true, + Timeout: 10 * time.Second, + }, + )) + ms.register("/metrics/json", jsonResponse(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + g := prometheus.ToTransactionalGatherer(ms.reg) + mfs, done, err := g.Gather() + defer done() + if err != nil { + msg := "could not gather metrics" + mainLog.Load().Warn().Err(err).Msg(msg) + http.Error(w, msg, http.StatusInternalServerError) + return + } + result := make([]*prom2json.Family, 0, len(mfs)) + for _, mf := range mfs { + result = append(result, prom2json.NewFamily(mf)) + } + if err := json.NewEncoder(w).Encode(result); err != nil { + msg := "could not marshal metrics result" + mainLog.Load().Warn().Err(err).Msg(msg) + http.Error(w, msg, http.StatusInternalServerError) + return + } + }))) +} + +// start runs the metricsServer. +func (ms *metricsServer) start() error { + listener, err := net.Listen("tcp", ms.addr) + if err != nil { + return err + } + go ms.server.Serve(listener) + ms.started = true + return nil +} + +// stop shutdowns the metricsServer within 2 seconds timeout. +func (ms *metricsServer) stop() error { + if !ms.started { + return nil + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) + defer cancel() + return ms.server.Shutdown(ctx) +} + +// runMetricsServer initializes metrics stats and runs the metrics server if enabled. +func (p *prog) runMetricsServer(ctx context.Context, reloadCh chan struct{}) { + if !p.metricsEnabled() { + return + } + + // Reset all stats. + statsVersion.Reset() + statsQueriesCount.Reset() + statsClientQueriesCount.Reset() + + reg := prometheus.NewRegistry() + // Register queries count stats if enabled. + if cfg.Service.MetricsQueryStats { + reg.MustRegister(statsQueriesCount) + reg.MustRegister(statsClientQueriesCount) + } + + addr := p.cfg.Service.MetricsListener + ms, err := newMetricsServer(addr, reg) + if err != nil { + mainLog.Load().Warn().Err(err).Msg("could not create new metrics server") + return + } + // Only start listener address if defined. + if addr != "" { + // Go runtime stats. + reg.MustRegister(collectors.NewBuildInfoCollector()) + reg.MustRegister(collectors.NewGoCollector( + collectors.WithGoCollectorRuntimeMetrics(collectors.MetricsAll), + )) + // ctrld stats. + reg.MustRegister(statsVersion) + statsVersion.WithLabelValues(commit, runtime.Version(), curVersion()).Inc() + reg.MustRegister(statsTimeStart) + statsTimeStart.Set(float64(time.Now().Unix())) + mainLog.Load().Debug().Msgf("starting metrics server on: %s", addr) + if err := ms.start(); err != nil { + mainLog.Load().Warn().Err(err).Msg("could not start metrics server") + return + } + } + + select { + case <-p.stopCh: + case <-ctx.Done(): + case <-reloadCh: + } + + if err := ms.stop(); err != nil { + mainLog.Load().Warn().Err(err).Msg("could not stop metrics server") + return + } +} diff --git a/cmd/cli/prog.go b/cmd/cli/prog.go index 0d4f645..4b3968f 100644 --- a/cmd/cli/prog.go +++ b/cmd/cli/prog.go @@ -348,6 +348,13 @@ func (p *prog) run(reload bool, reloadCh chan struct{}) { p.checkDnsLoopTicker(ctx) }() + wg.Add(1) + // Prometheus exporter goroutine. + go func() { + defer wg.Done() + p.runMetricsServer(ctx, reloadCh) + }() + if !reload { // Stop writing log to unix socket. consoleWriter.Out = os.Stdout @@ -365,6 +372,11 @@ func (p *prog) run(reload bool, reloadCh chan struct{}) { wg.Wait() } +// metricsEnabled reports whether prometheus exporter is enabled/disabled. +func (p *prog) metricsEnabled() bool { + return p.cfg.Service.MetricsQueryStats || p.cfg.Service.MetricsListener != "" +} + func (p *prog) Stop(s service.Service) error { mainLog.Load().Info().Msg("Service stopped") close(p.stopCh) diff --git a/cmd/cli/prometheus.go b/cmd/cli/prometheus.go new file mode 100644 index 0000000..fc2fc5d --- /dev/null +++ b/cmd/cli/prometheus.go @@ -0,0 +1,57 @@ +package cli + +import "github.com/prometheus/client_golang/prometheus" + +const ( + metricsLabelListener = "listener" + metricsLabelClientSourceIP = "client_source_ip" + metricsLabelClientMac = "client_mac" + metricsLabelClientHostname = "client_hostname" + metricsLabelUpstream = "upstream" + metricsLabelRRType = "rr_type" + metricsLabelRCode = "rcode" +) + +// statsVersion represent ctrld version. +var statsVersion = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "ctrld_build_info", + Help: "Version of ctrld process.", +}, []string{"gitref", "goversion", "version"}) + +// statsTimeStart represents start time of ctrld service. +var statsTimeStart = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "ctrld_time_seconds", + Help: "Start time of the ctrld process since unix epoch in seconds.", +}) + +var statsQueriesCountLabels = []string{ + metricsLabelListener, + metricsLabelClientSourceIP, + metricsLabelClientMac, + metricsLabelClientHostname, + metricsLabelUpstream, + metricsLabelRRType, + metricsLabelRCode, +} + +// statsQueriesCount counts total number of queries. +var statsQueriesCount = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "ctrld_queries_count", + Help: "Total number of queries.", +}, statsQueriesCountLabels) + +// statsClientQueriesCount counts total number of queries of a client. +// +// The labels "client_source_ip", "client_mac", "client_hostname" are unbounded, +// thus this stat is highly inefficient if there are many devices. +var statsClientQueriesCount = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "ctrld_client_queries_count", + Help: "Total number queries of a client.", +}, []string{metricsLabelClientSourceIP, metricsLabelClientMac, metricsLabelClientHostname}) + +// WithLabelValuesInc increases prometheus counter by 1 if query stats is enabled. +func (p *prog) WithLabelValuesInc(c *prometheus.CounterVec, lvs ...string) { + if p.cfg.Service.MetricsQueryStats { + c.WithLabelValues(lvs...).Inc() + } +} diff --git a/config.go b/config.go index 2ffec37..cb38096 100644 --- a/config.go +++ b/config.go @@ -195,6 +195,8 @@ type ServiceConfig struct { DiscoverHosts *bool `mapstructure:"discover_hosts" toml:"discover_hosts,omitempty"` DiscoverRefreshInterval int `mapstructure:"discover_refresh_interval" toml:"discover_refresh_interval,omitempty"` ClientIDPref string `mapstructure:"client_id_preference" toml:"client_id_preference,omitempty" validate:"omitempty,oneof=host mac"` + MetricsQueryStats bool `mapstructure:"metrics_query_stats" toml:"metrics_query_stats,omitempty"` + MetricsListener string `mapstructure:"metrics_listener" toml:"metrics_listener,omitempty"` Daemon bool `mapstructure:"-" toml:"-"` AllocateIP bool `mapstructure:"-" toml:"-"` } diff --git a/docs/config.md b/docs/config.md index b92e320..4f4d897 100644 --- a/docs/config.md +++ b/docs/config.md @@ -234,6 +234,20 @@ Else -> client ID will use both Mac and Hostname i.e. `hash(mac + host) - Valid values: `mac`, `host` - Default: "" +### metrics_query_stats +If set to `true`, collect and export the query counters, and show them in `clients list` command. + +- Type: boolean +- Required: no +- Default: false + +### metrics_listener +Specifying the `ip` and `port` of the metrics server. + +- Type: string +- Required: no +- Default: "" + ## Upstream The `[upstream]` section specifies the DNS upstream servers that `ctrld` will forward DNS requests to. diff --git a/go.mod b/go.mod index 4d38445..ebb62d2 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,8 @@ require ( github.com/miekg/dns v1.1.55 github.com/olekukonko/tablewriter v0.0.5 github.com/pelletier/go-toml/v2 v2.0.8 + github.com/prometheus/client_golang v1.15.1 + github.com/prometheus/prom2json v1.3.3 github.com/quic-go/quic-go v0.38.0 github.com/rs/zerolog v1.28.0 github.com/spf13/cobra v1.7.0 @@ -35,11 +37,14 @@ require ( require ( github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/golang/mock v1.6.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -52,6 +57,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 // indirect @@ -60,6 +66,9 @@ require ( github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/pierrec/lz4/v4 v4.1.17 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.4.0 // indirect + github.com/prometheus/common v0.44.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-20 v0.3.2 // indirect github.com/rivo/uniseg v0.4.4 // indirect @@ -76,6 +85,7 @@ require ( golang.org/x/mod v0.10.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/tools v0.9.1 // indirect + google.golang.org/protobuf v1.30.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a64357a..bbfe6c7 100644 --- a/go.sum +++ b/go.sum @@ -42,7 +42,11 @@ github.com/Windscribe/zerolog v0.0.0-20230503170159-e6aa153233be h1:qBKVRi7Mom5h github.com/Windscribe/zerolog v0.0.0-20230503170159-e6aa153233be/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -114,7 +118,9 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -126,6 +132,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -199,6 +206,8 @@ github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE= github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= github.com/mdlayher/ndp v1.0.1 h1:+yAD79/BWyFlvAoeG5ncPS0ItlHP/eVbH7bQ6/+LVA4= @@ -229,7 +238,17 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= +github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcETyaUgo= +github.com/prometheus/prom2json v1.3.3/go.mod h1:Pv4yIPktEkK7btWsrUTWDDDrnpUrAELaOCj+oFwlgmc= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-20 v0.3.2 h1:rRgN3WfnKbyik4dBV8A6girlJVxGand/d+jVKbQq5GI= @@ -610,7 +629,10 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/internal/clientinfo/client_info.go b/internal/clientinfo/client_info.go index 0e5c157..7f41fca 100644 --- a/internal/clientinfo/client_info.go +++ b/internal/clientinfo/client_info.go @@ -58,10 +58,12 @@ type ipLister interface { } type Client struct { - IP netip.Addr - Mac string - Hostname string - Source map[string]struct{} + IP netip.Addr + Mac string + Hostname string + Source map[string]struct{} + QueryCount int64 + IncludeQueryCount bool } type Table struct {