Skip to content

Commit 71a904f

Browse files
authored
* first cut at a "dnstapir-cli edm stats" command (#52)
* * first cut at a "dnstapir-cli edm stats" command * * changed to use an HTTP agent with a timeout to avoid infinite hangs if EDM isn't responding
1 parent eb15180 commit 71a904f

1 file changed

Lines changed: 164 additions & 0 deletions

File tree

cmd/edm.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* Copyright (c) 2024 Johan Stenstam, johan.stenstam@internetstiftelsen.se
3+
*/
4+
5+
package cmd
6+
7+
import (
8+
"bufio"
9+
"fmt"
10+
"io"
11+
"log"
12+
"net/http"
13+
"strconv"
14+
"strings"
15+
"time"
16+
17+
"github.com/ryanuber/columnize"
18+
"github.com/spf13/cobra"
19+
)
20+
21+
var EdmCmd = &cobra.Command{
22+
Use: "edm",
23+
Short: "Prefix command for EDM (Edge DNSTAP Minimiser) operations",
24+
}
25+
26+
var EdmStatsCmd = &cobra.Command{
27+
Use: "stats",
28+
Short: "Get statistics from the EDM metrics endpoint",
29+
Run: func(cmd *cobra.Command, args []string) {
30+
if len(args) != 0 {
31+
log.Fatal("stats must have no arguments")
32+
}
33+
34+
// Get the metrics endpoint URL (hardcoded in EDM as 127.0.0.1:2112)
35+
metricsURL := "http://127.0.0.1:2112/metrics"
36+
37+
// Create HTTP client with timeout
38+
client := &http.Client{
39+
Timeout: time.Second * 4,
40+
}
41+
42+
// Make HTTP GET request
43+
resp, err := client.Get(metricsURL)
44+
if err != nil {
45+
log.Fatalf("Error connecting to EDM metrics endpoint at %s: %v\n"+
46+
"Is EDM running?", metricsURL, err)
47+
}
48+
defer resp.Body.Close()
49+
50+
if resp.StatusCode != http.StatusOK {
51+
log.Fatalf("HTTP error from metrics endpoint: %s", resp.Status)
52+
}
53+
54+
// Parse Prometheus text format
55+
metrics, err := parsePrometheusMetrics(resp.Body)
56+
if err != nil {
57+
log.Fatalf("Error parsing metrics: %v", err)
58+
}
59+
60+
// Display EDM metrics in table format
61+
displayEdmMetrics(metrics)
62+
},
63+
}
64+
65+
func init() {
66+
EdmCmd.AddCommand(EdmStatsCmd)
67+
}
68+
69+
// parsePrometheusMetrics parses Prometheus text format and returns a map of metric name to value
70+
func parsePrometheusMetrics(r io.Reader) (map[string]float64, error) {
71+
metrics := make(map[string]float64)
72+
scanner := bufio.NewScanner(r)
73+
74+
for scanner.Scan() {
75+
line := strings.TrimSpace(scanner.Text())
76+
77+
// Skip empty lines and comments
78+
if line == "" || strings.HasPrefix(line, "#") {
79+
continue
80+
}
81+
82+
// Parse metric line: "metric_name value"
83+
// Can also have labels like: metric_name{label="value"} value
84+
parts := strings.Fields(line)
85+
if len(parts) < 2 {
86+
continue
87+
}
88+
89+
metricName := parts[0]
90+
// Remove any labels from metric name (everything after {)
91+
if idx := strings.Index(metricName, "{"); idx != -1 {
92+
metricName = metricName[:idx]
93+
}
94+
95+
value, err := strconv.ParseFloat(parts[1], 64)
96+
if err != nil {
97+
// Skip lines that can't be parsed
98+
continue
99+
}
100+
101+
metrics[metricName] = value
102+
}
103+
104+
if err := scanner.Err(); err != nil {
105+
return nil, err
106+
}
107+
108+
return metrics, nil
109+
}
110+
111+
// displayEdmMetrics displays EDM-specific metrics in a table format
112+
func displayEdmMetrics(metrics map[string]float64) {
113+
// Define the EDM metrics we want to display, in order
114+
edmMetrics := []struct {
115+
name string
116+
description string
117+
}{
118+
{"edm_processed_dnstap_total", "DNSTAP packets processed"},
119+
{"edm_new_qname_queued_total", "New qname events queued"},
120+
{"edm_new_qname_discarded_total", "New qname events discarded"},
121+
{"edm_new_qname_ch_len", "New qname channel buffer length"},
122+
{"edm_seen_qname_lru_evicted_total", "Qname LRU cache evictions"},
123+
{"edm_cryptopan_lru_hit_total", "Crypto-PAn LRU cache hits"},
124+
{"edm_cryptopan_lru_evicted_total", "Crypto-PAn LRU cache evictions"},
125+
{"edm_ignored_client_ip_total", "Packets ignored (client IP filter)"},
126+
{"edm_ignored_client_ip_error_total", "Client IP filter errors"},
127+
{"edm_ignored_question_name_total", "Packets ignored (question name filter)"},
128+
}
129+
130+
fmt.Println("EDM Statistics")
131+
fmt.Println()
132+
133+
out := []string{"Metric|Value|Description"}
134+
135+
for _, metric := range edmMetrics {
136+
value, exists := metrics[metric.name]
137+
if exists {
138+
// Format the metric name to remove "edm_" prefix for display
139+
displayName := strings.TrimPrefix(metric.name, "edm_")
140+
141+
// Format value based on whether it's a counter or gauge
142+
var valueStr string
143+
if value == float64(int64(value)) {
144+
// Integer value
145+
valueStr = fmt.Sprintf("%d", int64(value))
146+
} else {
147+
// Float value
148+
valueStr = fmt.Sprintf("%.2f", value)
149+
}
150+
151+
out = append(out, fmt.Sprintf("%s|%s|%s",
152+
displayName,
153+
valueStr,
154+
metric.description))
155+
}
156+
}
157+
158+
if len(out) == 1 {
159+
fmt.Println("No EDM metrics found")
160+
return
161+
}
162+
163+
fmt.Printf("%s\n", columnize.SimpleFormat(out))
164+
}

0 commit comments

Comments
 (0)