diff --git a/infrastructure/cdn-in-a-box/varnish/Dockerfile b/infrastructure/cdn-in-a-box/varnish/Dockerfile index e6f89188e4..12e2fb2728 100644 --- a/infrastructure/cdn-in-a-box/varnish/Dockerfile +++ b/infrastructure/cdn-in-a-box/varnish/Dockerfile @@ -41,6 +41,11 @@ RUN dnf install -y bind-utils kyotocabinet-libs initscripts iproute net-tools nm dnf install -y jq logrotate findutils && \ dnf clean all +# for building libvmod-dyncounters. +RUN yum -y config-manager --set-enabled powertools && yum install -y libtool automake make autoconf-archive python3.11 python3-docutils varnish-devel + +RUN git clone https://github.com/ehocdet/libvmod-dyncounters.git && cd libvmod-dyncounters && ./autogen.sh && ./configure && make && make install \ + && cd .. && rm -rf libvmod-dyncounters COPY infrastructure/cdn-in-a-box/varnish/run.sh infrastructure/cdn-in-a-box/traffic_ops/to-access.sh infrastructure/cdn-in-a-box/enroller/server_template.json / diff --git a/infrastructure/cdn-in-a-box/varnish/vstats.go b/infrastructure/cdn-in-a-box/varnish/vstats.go index 10c871717f..1f288e562d 100644 --- a/infrastructure/cdn-in-a-box/varnish/vstats.go +++ b/infrastructure/cdn-in-a-box/varnish/vstats.go @@ -31,12 +31,48 @@ import ( "strings" ) +type varnishstatOut struct { + Version int `json:"version"` + Timestamp string `json:"timestamp"` + Counters map[string]counter `json:"counters"` +} + +type counter struct { + Description string `json:"description"` + Flag string `json:"flag"` + Format string `json:"format"` + Value interface{} `json:"value"` +} + type vstats struct { - ProcLoadavg string `json:"proc.loadavg"` - ProcNetDev string `json:"proc.net.dev"` - InfSpeed int64 `json:"inf_speed"` - NotAvailable bool `json:"not_available"` - // TODO: stats + ProcLoadavg string `json:"proc.loadavg"` + ProcNetDev string `json:"proc.net.dev"` + InfSpeed int64 `json:"inf_speed"` + NotAvailable bool `json:"not_available"` + Stats map[string]interface{} `json:"stats"` +} + +func getVarnishCounters() map[string]interface{} { + stats := make(map[string]interface{}) + out, err := exec.Command("varnishstat", "-j", "-f", `TC*`).CombinedOutput() + if err != nil { + log.Printf("failed to execute varnishstat: %s\n", err) + return stats + } + varnishstatOut := varnishstatOut{} + if err := json.Unmarshal(out, &varnishstatOut); err != nil { + log.Printf("failed to parse varnishstat output: %s, due to error: %s\n", out, err) + return stats + } + for name, counter := range varnishstatOut.Counters { + counterName, ok := strings.CutPrefix(name, "TC.") + if !ok { + log.Printf("got counter without TC. prefix, that should not happen: %s", name) + continue + } + stats[counterName] = counter.Value + } + return stats } func getSystemData(inf string) vstats { @@ -90,7 +126,11 @@ func getStats(w http.ResponseWriter, r *http.Request) { } inf = strings.ReplaceAll(inf, ".", "") inf = strings.ReplaceAll(inf, "/", "") + vstats := getSystemData(inf) + stats := getVarnishCounters() + vstats.Stats = stats + encoder := json.NewEncoder(w) err := encoder.Encode(vstats) if err != nil { diff --git a/lib/varnishcfg/stats.go b/lib/varnishcfg/stats.go new file mode 100644 index 0000000000..2d7f9ec2cb --- /dev/null +++ b/lib/varnishcfg/stats.go @@ -0,0 +1,26 @@ +package varnishcfg + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +func (v VCLBuilder) configureStats(vclFile *vclFile) { + vclFile.imports = append(vclFile.imports, "dyncounters") + vclFile.subroutines["vcl_init"] = append(vclFile.subroutines["vcl_init"], "new TC = dyncounters.head();") + vclFile.subroutines["vcl_deliver"] = append(vclFile.subroutines["vcl_deliver"], "TC.incr(req.http.host, resp.status, 1);") +} diff --git a/lib/varnishcfg/vclbuilder.go b/lib/varnishcfg/vclbuilder.go index 480aaa686e..c6d1f46216 100644 --- a/lib/varnishcfg/vclbuilder.go +++ b/lib/varnishcfg/vclbuilder.go @@ -88,5 +88,7 @@ func (vb *VCLBuilder) BuildVCLFile() (string, []string, error) { dirWarnings, err := vb.configureDirectors(&v, parents) warnings = append(warnings, dirWarnings...) + vb.configureStats(&v) + return fmt.Sprint(v), warnings, err } diff --git a/traffic_monitor/cache/vstats.go b/traffic_monitor/cache/vstats.go index c977dedc9c..2d4277f967 100644 --- a/traffic_monitor/cache/vstats.go +++ b/traffic_monitor/cache/vstats.go @@ -23,6 +23,8 @@ import ( "errors" "fmt" "io" + "strconv" + "strings" "github.com/apache/trafficcontrol/lib/go-log" "github.com/apache/trafficcontrol/traffic_monitor/todata" @@ -75,5 +77,70 @@ func vstatsParse(cacheName string, r io.Reader, _ interface{}) (Statistics, map[ } func vstatsPrecompute(cacheName string, data todata.TOData, stats Statistics, miscStats map[string]interface{}) PrecomputedData { - return PrecomputedData{DeliveryServiceStats: map[string]*DSStat{}} + dsStats := make(map[string]*DSStat) + var precomputed PrecomputedData + precomputed.OutBytes = 0 + precomputed.MaxKbps = 0 + for _, iface := range stats.Interfaces { + precomputed.OutBytes += iface.BytesOut + kbps := iface.Speed * 1000 + if kbps > precomputed.MaxKbps { + precomputed.MaxKbps = kbps + } + } + + for name, value := range miscStats { + parts := strings.Split(name, ".") + subsubdomain := parts[0] + subdomain := parts[1] + domain := strings.Join(parts[2:len(parts)-1], ".") + + ds, ok := data.DeliveryServiceRegexes.DeliveryService(domain, subdomain, subsubdomain) + if !ok { + precomputed.Errors = append( + precomputed.Errors, + fmt.Errorf("no Delivery Service match for '%s.%s.%s'", subsubdomain, subdomain, domain), + ) + continue + } + if ds == "" { + precomputed.Errors = append( + precomputed.Errors, + fmt.Errorf("empty Delivery Service fqdn '%s.%s.%s'", subsubdomain, subdomain, domain), + ) + continue + } + + dsName := string(ds) + + vstatsProcessCounter(dsStats, dsName, parts[len(parts)-1], value) + } + precomputed.DeliveryServiceStats = dsStats + + return precomputed +} + +func vstatsProcessCounter(dsStats map[string]*DSStat, dsName, category string, value interface{}) error { + if stat, ok := dsStats[dsName]; stat == nil || !ok { + dsStats[dsName] = new(DSStat) + } + parsedValue, ok := value.(float64) + if !ok { + // only float counters are used now + return fmt.Errorf("expected counter value of type float got type: %T, for value: %v", value, value) + } + statusCode, err := strconv.ParseInt(category, 10, 64) + + if err == nil { + if statusCode >= 200 && statusCode < 300 { + dsStats[dsName].Status2xx += uint64(parsedValue) + } else if statusCode >= 300 && statusCode < 400 { + dsStats[dsName].Status3xx += uint64(parsedValue) + } else if statusCode >= 400 && statusCode < 500 { + dsStats[dsName].Status4xx += uint64(parsedValue) + } else if statusCode >= 500 && statusCode < 600 { + dsStats[dsName].Status5xx += uint64(parsedValue) + } + } + return nil } diff --git a/traffic_monitor/cache/vstats_test.go b/traffic_monitor/cache/vstats_test.go index d3f20aa6bc..bd8602e78a 100644 --- a/traffic_monitor/cache/vstats_test.go +++ b/traffic_monitor/cache/vstats_test.go @@ -20,8 +20,11 @@ package cache */ import ( + "reflect" "strings" "testing" + + "github.com/apache/trafficcontrol/traffic_monitor/todata" ) var vstatsData = `{ @@ -58,3 +61,47 @@ func TestVstatsParse(t *testing.T) { t.Errorf("expected NotAvailable to be false") } } + +func TestVstatsPrecompute(t *testing.T) { + to := todata.New() + to.DeliveryServiceRegexes.DirectMatches["infra.origin.ciab.test"] = "demo1" + stats := Statistics{ + Interfaces: map[string]Interface{ + "eth0": { + Speed: 1000, + BytesIn: 2000, + BytesOut: 3000, + }, + "eth1": { + Speed: 2000, + BytesIn: 3000, + BytesOut: 4000, + }, + }, + } + counters := map[string]interface{}{ + "infra.origin.ciab.test.200": float64(5), + "infra.origin.ciab.test.201": float64(5), + "infra.origin.ciab.test.300": float64(6), + "infra.origin.ciab.test.404": float64(3), + "not-found.origin.ciab.test.404": float64(10), // should not affect other stats and return error + } + dsDtats := map[string]*DSStat{ + "demo1": {Status2xx: 10, Status3xx: 6, Status4xx: 3}, + } + precomputedData := vstatsPrecompute("cache", *to, stats, counters) + + if !reflect.DeepEqual(precomputedData.DeliveryServiceStats, dsDtats) { + t.Errorf("expected %v got %v", dsDtats, precomputedData.DeliveryServiceStats) + } + if precomputedData.OutBytes != 7000 { + t.Errorf("expected 7000 got %d", precomputedData.OutBytes) + } + if precomputedData.MaxKbps != 2000000 { + t.Errorf("expected 2000000 got %d", precomputedData.MaxKbps) + } + if len(precomputedData.Errors) != 1 { + t.Errorf("expected one error got %v", precomputedData.Errors) + } + +}