RELEASE 1.0.1: v2 log format, source_tag-labeled metrics, lint cleanup
Wire-format and metric overhaul. Both file and UDP ingest now share one
versioned ParseLine that dispatches on the v<N>\t prefix; v1 stays
unchanged, v2 adds $bytes_sent (replacing $body_bytes_sent),
$request_length, $upstream_response_time, and $upstream_status. File
ingest gains the same versioning, and the legacy positional file format
is removed (no live deployments).
Prometheus exposition is rewritten:
- nginx_http_bytes_sent and nginx_http_request_duration_seconds gain
a source_tag label.
- nginx_http_requests_by_source_total gains status_class.
- New v2-only metrics: nginx_http_request_bytes,
nginx_http_upstream_duration_seconds,
nginx_http_upstream_requests_total{status_class}.
- Dropped nginx_http_response_body_bytes_by_source (subsumed by the
dual-labeled bytes_sent metric).
Adds 'make fixstyle' (gofmt -w) and clears all golangci-lint findings
across the repo (errcheck, S1001, ST1005, unused).
Docs in design.md FR-2/FR-8 and user-guide.md are rewritten to present
v2 as the recommended log format.
This commit is contained in:
+252
-135
@@ -14,27 +14,46 @@ const promNumBodyBounds = 7
|
||||
|
||||
var promBodyBounds = [promNumBodyBounds]int64{256, 1024, 4096, 16384, 65536, 262144, 1048576}
|
||||
|
||||
// Request-time histogram bucket upper bounds in seconds (standard Prometheus defaults).
|
||||
// Duration histogram bucket upper bounds in seconds (Prometheus defaults).
|
||||
const promNumTimeBounds = 11
|
||||
|
||||
var promTimeBounds = [promNumTimeBounds]float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10}
|
||||
|
||||
const promCounterCap = 250_000 // safety cap on {host,method,status} counter entries
|
||||
|
||||
// promCounterKey is the label set for per-request counters.
|
||||
// promCounterKey is the label set for the per-request counter.
|
||||
type promCounterKey struct {
|
||||
Host string
|
||||
Method string
|
||||
Status string
|
||||
}
|
||||
|
||||
// promBodyEntry holds the body_bytes_sent histogram for one host.
|
||||
// hostSourceKey labels histograms by {host, source_tag}.
|
||||
type hostSourceKey struct {
|
||||
Host string
|
||||
SourceTag string
|
||||
}
|
||||
|
||||
// sourceClassKey labels the source-tag rollup counter.
|
||||
type sourceClassKey struct {
|
||||
SourceTag string
|
||||
StatusClass string
|
||||
}
|
||||
|
||||
// upstreamKey labels the upstream-only request counter.
|
||||
type upstreamKey struct {
|
||||
Host string
|
||||
SourceTag string
|
||||
StatusClass string // class of $upstream_status
|
||||
}
|
||||
|
||||
// promBodyEntry holds one body-size histogram (one label-set worth).
|
||||
type promBodyEntry struct {
|
||||
buckets [promNumBodyBounds + 1]int64 // indices 0..N-1: le=bound[i]; index N: le=+Inf
|
||||
sum int64
|
||||
}
|
||||
|
||||
// promTimeEntry holds the request_time histogram for one host.
|
||||
// promTimeEntry holds one duration histogram (one label-set worth).
|
||||
type promTimeEntry struct {
|
||||
buckets [promNumTimeBounds + 1]int64
|
||||
sum float64
|
||||
@@ -45,30 +64,31 @@ type promTimeEntry struct {
|
||||
// Ingest must be called from exactly one goroutine (the store's Run goroutine).
|
||||
// ServeHTTP may be called from any number of goroutines concurrently.
|
||||
type PromStore struct {
|
||||
mu sync.Mutex
|
||||
counters map[promCounterKey]int64
|
||||
body map[string]*promBodyEntry // keyed by host
|
||||
reqTime map[string]*promTimeEntry // keyed by host
|
||||
mu sync.Mutex
|
||||
counters map[promCounterKey]int64
|
||||
bytesSent map[hostSourceKey]*promBodyEntry
|
||||
requestDuration map[hostSourceKey]*promTimeEntry
|
||||
requestBytes map[hostSourceKey]*promBodyEntry // v2 only
|
||||
upstreamDuration map[hostSourceKey]*promTimeEntry // v2 only
|
||||
upstreamCounters map[upstreamKey]int64 // v2 only
|
||||
sourceCounters map[sourceClassKey]int64
|
||||
|
||||
// per-source_tag rollups (parallel series, not a cross-product with host)
|
||||
sourceCounters map[string]int64 // keyed by source_tag
|
||||
sourceBody map[string]*promBodyEntry // keyed by source_tag
|
||||
|
||||
// UDP ingest counters — protected by their own atomic-friendly lock.
|
||||
udpMu sync.Mutex
|
||||
udpPacketsReceived int64 // datagrams read off the socket
|
||||
udpLoglinesSuccess int64 // successfully parsed
|
||||
udpLoglinesConsumed int64 // successfully forwarded to the store channel
|
||||
udpMu sync.Mutex
|
||||
udpPacketsReceived int64
|
||||
udpLoglinesSuccess int64
|
||||
udpLoglinesConsumed int64
|
||||
}
|
||||
|
||||
// NewPromStore returns an empty PromStore ready for use.
|
||||
func NewPromStore() *PromStore {
|
||||
return &PromStore{
|
||||
counters: make(map[promCounterKey]int64, 1024),
|
||||
body: make(map[string]*promBodyEntry, 64),
|
||||
reqTime: make(map[string]*promTimeEntry, 64),
|
||||
sourceCounters: make(map[string]int64, 32),
|
||||
sourceBody: make(map[string]*promBodyEntry, 32),
|
||||
counters: make(map[promCounterKey]int64, 1024),
|
||||
bytesSent: make(map[hostSourceKey]*promBodyEntry, 64),
|
||||
requestDuration: make(map[hostSourceKey]*promTimeEntry, 64),
|
||||
requestBytes: make(map[hostSourceKey]*promBodyEntry, 64),
|
||||
upstreamDuration: make(map[hostSourceKey]*promTimeEntry, 64),
|
||||
upstreamCounters: make(map[upstreamKey]int64, 64),
|
||||
sourceCounters: make(map[sourceClassKey]int64, 32),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,8 +96,11 @@ func NewPromStore() *PromStore {
|
||||
// Must be called from a single goroutine.
|
||||
func (p *PromStore) Ingest(r LogRecord) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
// --- per-{host,method,status} request counter ---
|
||||
hsk := hostSourceKey{Host: r.Website, SourceTag: r.SourceTag}
|
||||
|
||||
// nginx_http_requests_total{host,method,status} — capped.
|
||||
ck := promCounterKey{Host: r.Website, Method: r.Method, Status: r.Status}
|
||||
if _, ok := p.counters[ck]; ok {
|
||||
p.counters[ck]++
|
||||
@@ -85,37 +108,54 @@ func (p *PromStore) Ingest(r LogRecord) {
|
||||
p.counters[ck] = 1
|
||||
}
|
||||
|
||||
// --- body_bytes_sent histogram (keyed by host only) ---
|
||||
observeBody(p.body, r.Website, r.BodyBytesSent)
|
||||
|
||||
// --- request_time histogram (keyed by host only) ---
|
||||
te, ok := p.reqTime[r.Website]
|
||||
if !ok {
|
||||
te = &promTimeEntry{}
|
||||
p.reqTime[r.Website] = te
|
||||
observeBody(p.bytesSent, hsk, r.BytesSent)
|
||||
observeTime(p.requestDuration, hsk, r.RequestTime)
|
||||
if r.RequestLength > 0 {
|
||||
observeBody(p.requestBytes, hsk, r.RequestLength)
|
||||
}
|
||||
for i, bound := range promTimeBounds {
|
||||
if r.RequestTime <= bound {
|
||||
te.buckets[i]++
|
||||
}
|
||||
|
||||
p.sourceCounters[sourceClassKey{
|
||||
SourceTag: r.SourceTag,
|
||||
StatusClass: statusClass(r.Status),
|
||||
}]++
|
||||
|
||||
if r.HasUpstream {
|
||||
observeTime(p.upstreamDuration, hsk, r.UpstreamResponseTime)
|
||||
p.upstreamCounters[upstreamKey{
|
||||
Host: r.Website,
|
||||
SourceTag: r.SourceTag,
|
||||
StatusClass: statusClass(r.UpstreamStatus),
|
||||
}]++
|
||||
}
|
||||
te.buckets[promNumTimeBounds]++ // +Inf
|
||||
te.sum += r.RequestTime
|
||||
|
||||
// --- per-source_tag rollups ---
|
||||
p.sourceCounters[r.SourceTag]++
|
||||
observeBody(p.sourceBody, r.SourceTag, r.BodyBytesSent)
|
||||
|
||||
p.mu.Unlock()
|
||||
}
|
||||
|
||||
// IncUDPPacket, IncUDPSuccess, and IncUDPConsumed bump their respective
|
||||
// UDP ingest counters. They are called from the UDP listener goroutine.
|
||||
// IncUDPPacket, IncUDPSuccess, IncUDPConsumed bump UDP-ingest counters from
|
||||
// the listener goroutine.
|
||||
func (p *PromStore) IncUDPPacket() { p.udpMu.Lock(); p.udpPacketsReceived++; p.udpMu.Unlock() }
|
||||
func (p *PromStore) IncUDPSuccess() { p.udpMu.Lock(); p.udpLoglinesSuccess++; p.udpMu.Unlock() }
|
||||
func (p *PromStore) IncUDPConsumed() { p.udpMu.Lock(); p.udpLoglinesConsumed++; p.udpMu.Unlock() }
|
||||
|
||||
func observeBody(m map[string]*promBodyEntry, key string, bytes int64) {
|
||||
// statusClass folds an HTTP status code into 2xx/3xx/4xx/5xx, with anything
|
||||
// else falling to "other" (including empty input).
|
||||
func statusClass(status string) string {
|
||||
if status == "" {
|
||||
return "other"
|
||||
}
|
||||
switch status[0] {
|
||||
case '2':
|
||||
return "2xx"
|
||||
case '3':
|
||||
return "3xx"
|
||||
case '4':
|
||||
return "4xx"
|
||||
case '5':
|
||||
return "5xx"
|
||||
default:
|
||||
return "other"
|
||||
}
|
||||
}
|
||||
|
||||
func observeBody(m map[hostSourceKey]*promBodyEntry, key hostSourceKey, bytes int64) {
|
||||
e, ok := m[key]
|
||||
if !ok {
|
||||
e = &promBodyEntry{}
|
||||
@@ -126,53 +166,77 @@ func observeBody(m map[string]*promBodyEntry, key string, bytes int64) {
|
||||
e.buckets[i]++
|
||||
}
|
||||
}
|
||||
e.buckets[promNumBodyBounds]++ // +Inf
|
||||
e.buckets[promNumBodyBounds]++
|
||||
e.sum += bytes
|
||||
}
|
||||
|
||||
func observeTime(m map[hostSourceKey]*promTimeEntry, key hostSourceKey, seconds float64) {
|
||||
e, ok := m[key]
|
||||
if !ok {
|
||||
e = &promTimeEntry{}
|
||||
m[key] = e
|
||||
}
|
||||
for i, bound := range promTimeBounds {
|
||||
if seconds <= bound {
|
||||
e.buckets[i]++
|
||||
}
|
||||
}
|
||||
e.buckets[promNumTimeBounds]++
|
||||
e.sum += seconds
|
||||
}
|
||||
|
||||
// ServeHTTP renders all metrics in the Prometheus text exposition format (0.0.4).
|
||||
func (p *PromStore) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
|
||||
// Snapshot everything under the lock, then render without holding it.
|
||||
p.mu.Lock()
|
||||
|
||||
type counterSnap struct {
|
||||
k promCounterKey
|
||||
v int64
|
||||
}
|
||||
type bodySnap struct {
|
||||
k hostSourceKey
|
||||
e promBodyEntry
|
||||
}
|
||||
type timeSnap struct {
|
||||
k hostSourceKey
|
||||
e promTimeEntry
|
||||
}
|
||||
type upstreamCounterSnap struct {
|
||||
k upstreamKey
|
||||
v int64
|
||||
}
|
||||
type sourceCounterSnap struct {
|
||||
k sourceClassKey
|
||||
v int64
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
|
||||
counters := make([]counterSnap, 0, len(p.counters))
|
||||
for k, v := range p.counters {
|
||||
counters = append(counters, counterSnap{k, v})
|
||||
}
|
||||
|
||||
type bodySnap struct {
|
||||
label string
|
||||
e promBodyEntry
|
||||
bytesSnaps := make([]bodySnap, 0, len(p.bytesSent))
|
||||
for k, e := range p.bytesSent {
|
||||
bytesSnaps = append(bytesSnaps, bodySnap{k, *e})
|
||||
}
|
||||
bodySnaps := make([]bodySnap, 0, len(p.body))
|
||||
for h, e := range p.body {
|
||||
bodySnaps = append(bodySnaps, bodySnap{h, *e})
|
||||
requestBytesSnaps := make([]bodySnap, 0, len(p.requestBytes))
|
||||
for k, e := range p.requestBytes {
|
||||
requestBytesSnaps = append(requestBytesSnaps, bodySnap{k, *e})
|
||||
}
|
||||
|
||||
type timeSnap struct {
|
||||
host string
|
||||
e promTimeEntry
|
||||
requestDurationSnaps := make([]timeSnap, 0, len(p.requestDuration))
|
||||
for k, e := range p.requestDuration {
|
||||
requestDurationSnaps = append(requestDurationSnaps, timeSnap{k, *e})
|
||||
}
|
||||
timeSnaps := make([]timeSnap, 0, len(p.reqTime))
|
||||
for h, e := range p.reqTime {
|
||||
timeSnaps = append(timeSnaps, timeSnap{h, *e})
|
||||
upstreamDurationSnaps := make([]timeSnap, 0, len(p.upstreamDuration))
|
||||
for k, e := range p.upstreamDuration {
|
||||
upstreamDurationSnaps = append(upstreamDurationSnaps, timeSnap{k, *e})
|
||||
}
|
||||
|
||||
type sourceCounterSnap struct {
|
||||
tag string
|
||||
v int64
|
||||
upstreamCounters := make([]upstreamCounterSnap, 0, len(p.upstreamCounters))
|
||||
for k, v := range p.upstreamCounters {
|
||||
upstreamCounters = append(upstreamCounters, upstreamCounterSnap{k, v})
|
||||
}
|
||||
sourceCounters := make([]sourceCounterSnap, 0, len(p.sourceCounters))
|
||||
for t, v := range p.sourceCounters {
|
||||
sourceCounters = append(sourceCounters, sourceCounterSnap{t, v})
|
||||
}
|
||||
sourceBodySnaps := make([]bodySnap, 0, len(p.sourceBody))
|
||||
for t, e := range p.sourceBody {
|
||||
sourceBodySnaps = append(sourceBodySnaps, bodySnap{t, *e})
|
||||
for k, v := range p.sourceCounters {
|
||||
sourceCounters = append(sourceCounters, sourceCounterSnap{k, v})
|
||||
}
|
||||
|
||||
p.mu.Unlock()
|
||||
@@ -183,7 +247,6 @@ func (p *PromStore) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
|
||||
udpConsumed := p.udpLoglinesConsumed
|
||||
p.udpMu.Unlock()
|
||||
|
||||
// Sort for stable, human-readable output.
|
||||
sort.Slice(counters, func(i, j int) bool {
|
||||
a, b := counters[i].k, counters[j].k
|
||||
if a.Host != b.Host {
|
||||
@@ -194,85 +257,139 @@ func (p *PromStore) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
|
||||
}
|
||||
return a.Status < b.Status
|
||||
})
|
||||
sort.Slice(bodySnaps, func(i, j int) bool { return bodySnaps[i].label < bodySnaps[j].label })
|
||||
sort.Slice(timeSnaps, func(i, j int) bool { return timeSnaps[i].host < timeSnaps[j].host })
|
||||
sort.Slice(sourceCounters, func(i, j int) bool { return sourceCounters[i].tag < sourceCounters[j].tag })
|
||||
sort.Slice(sourceBodySnaps, func(i, j int) bool { return sourceBodySnaps[i].label < sourceBodySnaps[j].label })
|
||||
sortBody := func(s []bodySnap) {
|
||||
sort.Slice(s, func(i, j int) bool {
|
||||
a, b := s[i].k, s[j].k
|
||||
if a.Host != b.Host {
|
||||
return a.Host < b.Host
|
||||
}
|
||||
return a.SourceTag < b.SourceTag
|
||||
})
|
||||
}
|
||||
sortTime := func(s []timeSnap) {
|
||||
sort.Slice(s, func(i, j int) bool {
|
||||
a, b := s[i].k, s[j].k
|
||||
if a.Host != b.Host {
|
||||
return a.Host < b.Host
|
||||
}
|
||||
return a.SourceTag < b.SourceTag
|
||||
})
|
||||
}
|
||||
sortBody(bytesSnaps)
|
||||
sortBody(requestBytesSnaps)
|
||||
sortTime(requestDurationSnaps)
|
||||
sortTime(upstreamDurationSnaps)
|
||||
sort.Slice(upstreamCounters, func(i, j int) bool {
|
||||
a, b := upstreamCounters[i].k, upstreamCounters[j].k
|
||||
if a.Host != b.Host {
|
||||
return a.Host < b.Host
|
||||
}
|
||||
if a.SourceTag != b.SourceTag {
|
||||
return a.SourceTag < b.SourceTag
|
||||
}
|
||||
return a.StatusClass < b.StatusClass
|
||||
})
|
||||
sort.Slice(sourceCounters, func(i, j int) bool {
|
||||
a, b := sourceCounters[i].k, sourceCounters[j].k
|
||||
if a.SourceTag != b.SourceTag {
|
||||
return a.SourceTag < b.SourceTag
|
||||
}
|
||||
return a.StatusClass < b.StatusClass
|
||||
})
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
|
||||
bw := bufio.NewWriterSize(w, 256*1024)
|
||||
|
||||
// nginx_http_requests_total
|
||||
fmt.Fprintln(bw, "# HELP nginx_http_requests_total Total number of HTTP requests processed.")
|
||||
fmt.Fprintln(bw, "# TYPE nginx_http_requests_total counter")
|
||||
// pf, pln are short helpers so the metric block reads cleanly. Errors on a
|
||||
// bufio writer wrapping http.ResponseWriter mean the client disconnected;
|
||||
// there's nothing useful to do mid-write — the next call will simply no-op.
|
||||
pf := func(format string, a ...any) { _, _ = fmt.Fprintf(bw, format, a...) }
|
||||
pln := func(s string) { _, _ = fmt.Fprintln(bw, s) }
|
||||
|
||||
pln("# HELP nginx_http_requests_total Total number of HTTP requests processed.")
|
||||
pln("# TYPE nginx_http_requests_total counter")
|
||||
for _, c := range counters {
|
||||
fmt.Fprintf(bw, "nginx_http_requests_total{host=%q,method=%q,status=%q} %d\n",
|
||||
pf("nginx_http_requests_total{host=%q,method=%q,status=%q} %d\n",
|
||||
c.k.Host, c.k.Method, c.k.Status, c.v)
|
||||
}
|
||||
|
||||
// nginx_http_response_body_bytes (histogram, labeled by host)
|
||||
fmt.Fprintln(bw, "# HELP nginx_http_response_body_bytes HTTP response body size distribution in bytes.")
|
||||
fmt.Fprintln(bw, "# TYPE nginx_http_response_body_bytes histogram")
|
||||
for _, s := range bodySnaps {
|
||||
writeBodyHistogram(bw, "nginx_http_response_body_bytes", "host", s.label, s.e)
|
||||
pln("# HELP nginx_http_bytes_sent HTTP response size distribution in bytes (body for v1 records, full wire bytes for v2).")
|
||||
pln("# TYPE nginx_http_bytes_sent histogram")
|
||||
for _, s := range bytesSnaps {
|
||||
writeBodyHistogramHS(bw, "nginx_http_bytes_sent", s.k, s.e)
|
||||
}
|
||||
|
||||
// nginx_http_request_duration_seconds (histogram, labeled by host)
|
||||
fmt.Fprintln(bw, "# HELP nginx_http_request_duration_seconds HTTP request processing time in seconds.")
|
||||
fmt.Fprintln(bw, "# TYPE nginx_http_request_duration_seconds histogram")
|
||||
for _, s := range timeSnaps {
|
||||
for i, bound := range promTimeBounds {
|
||||
fmt.Fprintf(bw, "nginx_http_request_duration_seconds_bucket{host=%q,le=%q} %d\n",
|
||||
s.host, formatFloat(bound), s.e.buckets[i])
|
||||
}
|
||||
fmt.Fprintf(bw, "nginx_http_request_duration_seconds_bucket{host=%q,le=\"+Inf\"} %d\n",
|
||||
s.host, s.e.buckets[promNumTimeBounds])
|
||||
fmt.Fprintf(bw, "nginx_http_request_duration_seconds_count{host=%q} %d\n",
|
||||
s.host, s.e.buckets[promNumTimeBounds])
|
||||
fmt.Fprintf(bw, "nginx_http_request_duration_seconds_sum{host=%q} %g\n",
|
||||
s.host, s.e.sum)
|
||||
pln("# HELP nginx_http_request_bytes HTTP request size distribution in bytes (v2 emitters only).")
|
||||
pln("# TYPE nginx_http_request_bytes histogram")
|
||||
for _, s := range requestBytesSnaps {
|
||||
writeBodyHistogramHS(bw, "nginx_http_request_bytes", s.k, s.e)
|
||||
}
|
||||
|
||||
// nginx_http_requests_by_source_total (counter, labeled by source_tag)
|
||||
fmt.Fprintln(bw, "# HELP nginx_http_requests_by_source_total HTTP requests rolled up by nginx source tag.")
|
||||
fmt.Fprintln(bw, "# TYPE nginx_http_requests_by_source_total counter")
|
||||
pln("# HELP nginx_http_request_duration_seconds HTTP request processing time in seconds.")
|
||||
pln("# TYPE nginx_http_request_duration_seconds histogram")
|
||||
for _, s := range requestDurationSnaps {
|
||||
writeTimeHistogramHS(bw, "nginx_http_request_duration_seconds", s.k, s.e)
|
||||
}
|
||||
|
||||
pln("# HELP nginx_http_upstream_duration_seconds Upstream response time in seconds (v2 emitters only).")
|
||||
pln("# TYPE nginx_http_upstream_duration_seconds histogram")
|
||||
for _, s := range upstreamDurationSnaps {
|
||||
writeTimeHistogramHS(bw, "nginx_http_upstream_duration_seconds", s.k, s.e)
|
||||
}
|
||||
|
||||
pln("# HELP nginx_http_upstream_requests_total Requests served via an upstream, by upstream-status class (v2 emitters only).")
|
||||
pln("# TYPE nginx_http_upstream_requests_total counter")
|
||||
for _, c := range upstreamCounters {
|
||||
pf("nginx_http_upstream_requests_total{host=%q,source_tag=%q,status_class=%q} %d\n",
|
||||
c.k.Host, c.k.SourceTag, c.k.StatusClass, c.v)
|
||||
}
|
||||
|
||||
pln("# HELP nginx_http_requests_by_source_total HTTP requests rolled up by source_tag and status class.")
|
||||
pln("# TYPE nginx_http_requests_by_source_total counter")
|
||||
for _, c := range sourceCounters {
|
||||
fmt.Fprintf(bw, "nginx_http_requests_by_source_total{source_tag=%q} %d\n", c.tag, c.v)
|
||||
pf("nginx_http_requests_by_source_total{source_tag=%q,status_class=%q} %d\n",
|
||||
c.k.SourceTag, c.k.StatusClass, c.v)
|
||||
}
|
||||
|
||||
// nginx_http_response_body_bytes_by_source (histogram, labeled by source_tag)
|
||||
fmt.Fprintln(bw, "# HELP nginx_http_response_body_bytes_by_source HTTP response body size distribution by nginx source tag.")
|
||||
fmt.Fprintln(bw, "# TYPE nginx_http_response_body_bytes_by_source histogram")
|
||||
for _, s := range sourceBodySnaps {
|
||||
writeBodyHistogram(bw, "nginx_http_response_body_bytes_by_source", "source_tag", s.label, s.e)
|
||||
}
|
||||
pln("# HELP logtail_udp_packets_received_total Datagrams read from the UDP socket.")
|
||||
pln("# TYPE logtail_udp_packets_received_total counter")
|
||||
pf("logtail_udp_packets_received_total %d\n", udpPackets)
|
||||
pln("# HELP logtail_udp_loglines_success_total UDP loglines that parsed successfully.")
|
||||
pln("# TYPE logtail_udp_loglines_success_total counter")
|
||||
pf("logtail_udp_loglines_success_total %d\n", udpSuccess)
|
||||
pln("# HELP logtail_udp_loglines_consumed_total UDP loglines forwarded to the store (not dropped).")
|
||||
pln("# TYPE logtail_udp_loglines_consumed_total counter")
|
||||
pf("logtail_udp_loglines_consumed_total %d\n", udpConsumed)
|
||||
|
||||
// UDP ingest counters — lets operators distinguish parse failures
|
||||
// (received - success) from channel-full drops (success - consumed).
|
||||
fmt.Fprintln(bw, "# HELP logtail_udp_packets_received_total Datagrams read from the UDP socket.")
|
||||
fmt.Fprintln(bw, "# TYPE logtail_udp_packets_received_total counter")
|
||||
fmt.Fprintf(bw, "logtail_udp_packets_received_total %d\n", udpPackets)
|
||||
fmt.Fprintln(bw, "# HELP logtail_udp_loglines_success_total UDP loglines that parsed successfully.")
|
||||
fmt.Fprintln(bw, "# TYPE logtail_udp_loglines_success_total counter")
|
||||
fmt.Fprintf(bw, "logtail_udp_loglines_success_total %d\n", udpSuccess)
|
||||
fmt.Fprintln(bw, "# HELP logtail_udp_loglines_consumed_total UDP loglines forwarded to the store (not dropped).")
|
||||
fmt.Fprintln(bw, "# TYPE logtail_udp_loglines_consumed_total counter")
|
||||
fmt.Fprintf(bw, "logtail_udp_loglines_consumed_total %d\n", udpConsumed)
|
||||
|
||||
bw.Flush()
|
||||
_ = bw.Flush()
|
||||
}
|
||||
|
||||
func writeBodyHistogram(bw *bufio.Writer, metric, labelName, labelValue string, e promBodyEntry) {
|
||||
func writeBodyHistogramHS(bw *bufio.Writer, metric string, k hostSourceKey, e promBodyEntry) {
|
||||
pf := func(format string, a ...any) { _, _ = fmt.Fprintf(bw, format, a...) }
|
||||
for i, bound := range promBodyBounds {
|
||||
fmt.Fprintf(bw, "%s_bucket{%s=%q,le=%q} %d\n",
|
||||
metric, labelName, labelValue, fmt.Sprintf("%d", bound), e.buckets[i])
|
||||
pf("%s_bucket{host=%q,source_tag=%q,le=\"%d\"} %d\n",
|
||||
metric, k.Host, k.SourceTag, bound, e.buckets[i])
|
||||
}
|
||||
fmt.Fprintf(bw, "%s_bucket{%s=%q,le=\"+Inf\"} %d\n",
|
||||
metric, labelName, labelValue, e.buckets[promNumBodyBounds])
|
||||
fmt.Fprintf(bw, "%s_count{%s=%q} %d\n",
|
||||
metric, labelName, labelValue, e.buckets[promNumBodyBounds])
|
||||
fmt.Fprintf(bw, "%s_sum{%s=%q} %d\n",
|
||||
metric, labelName, labelValue, e.sum)
|
||||
pf("%s_bucket{host=%q,source_tag=%q,le=\"+Inf\"} %d\n",
|
||||
metric, k.Host, k.SourceTag, e.buckets[promNumBodyBounds])
|
||||
pf("%s_count{host=%q,source_tag=%q} %d\n",
|
||||
metric, k.Host, k.SourceTag, e.buckets[promNumBodyBounds])
|
||||
pf("%s_sum{host=%q,source_tag=%q} %d\n",
|
||||
metric, k.Host, k.SourceTag, e.sum)
|
||||
}
|
||||
|
||||
func writeTimeHistogramHS(bw *bufio.Writer, metric string, k hostSourceKey, e promTimeEntry) {
|
||||
pf := func(format string, a ...any) { _, _ = fmt.Fprintf(bw, format, a...) }
|
||||
for i, bound := range promTimeBounds {
|
||||
pf("%s_bucket{host=%q,source_tag=%q,le=%q} %d\n",
|
||||
metric, k.Host, k.SourceTag, formatFloat(bound), e.buckets[i])
|
||||
}
|
||||
pf("%s_bucket{host=%q,source_tag=%q,le=\"+Inf\"} %d\n",
|
||||
metric, k.Host, k.SourceTag, e.buckets[promNumTimeBounds])
|
||||
pf("%s_count{host=%q,source_tag=%q} %d\n",
|
||||
metric, k.Host, k.SourceTag, e.buckets[promNumTimeBounds])
|
||||
pf("%s_sum{host=%q,source_tag=%q} %g\n",
|
||||
metric, k.Host, k.SourceTag, e.sum)
|
||||
}
|
||||
|
||||
// formatFloat renders a float64 bucket bound without trailing zeros but always
|
||||
@@ -280,7 +397,7 @@ func writeBodyHistogram(bw *bufio.Writer, metric, labelName, labelValue string,
|
||||
func formatFloat(f float64) string {
|
||||
s := fmt.Sprintf("%g", f)
|
||||
if !strings.Contains(s, ".") && !strings.Contains(s, "e") {
|
||||
s += ".0" // ensure it looks like a float, not an integer
|
||||
s += ".0"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user