Skip to content
This repository was archived by the owner on Nov 24, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions infrastructure/cdn-in-a-box/varnish/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 /

Expand Down
50 changes: 45 additions & 5 deletions infrastructure/cdn-in-a-box/varnish/vstats.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
26 changes: 26 additions & 0 deletions lib/varnishcfg/stats.go
Original file line number Diff line number Diff line change
@@ -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);")
}
2 changes: 2 additions & 0 deletions lib/varnishcfg/vclbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
69 changes: 68 additions & 1 deletion traffic_monitor/cache/vstats.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"errors"
"fmt"
"io"
"strconv"
"strings"

"github.com/apache/trafficcontrol/lib/go-log"
"github.com/apache/trafficcontrol/traffic_monitor/todata"
Expand Down Expand Up @@ -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
}
47 changes: 47 additions & 0 deletions traffic_monitor/cache/vstats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ package cache
*/

import (
"reflect"
"strings"
"testing"

"github.com/apache/trafficcontrol/traffic_monitor/todata"
)

var vstatsData = `{
Expand Down Expand Up @@ -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)
}

}