5 Commits

Author SHA1 Message Date
skamboj
7935a11f9d Merge pull request #164 from cooperlees/master
Add UDP probe metrics: packet loss, hop count, and RTT
2026-04-03 13:04:31 -04:00
Sachin Kamboj
de7f4e9004 Bump the version to 3.11.0
Signed-off-by: Sachin Kamboj <skamboj1@bloomberg.net>
2026-04-03 12:57:41 -04:00
Cooper Ry Lees
145d2bf000 Rename PathLength to HopCount in swagger model and UI
Rename the swagger field from path-length to hop-count so the
generated Go struct field (PathLength → HopCount) and JSON key
(path-length → hop-count) align with the Prometheus metric rename
to goldpinger_peers_hop_count from the previous commit.

Signed-off-by: Cooper Ry Lees <me@cooperlees.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:45:31 +00:00
Cooper Ry Lees
641b658f23 Address PR #164 review feedback
Concurrent HTTP + UDP pings:
  HTTP ping and UDP probe now run in separate goroutines via
  sync.WaitGroup, so UDP timeout doesn't add to the ping cycle
  latency. (skamboj on pinger.go:124)

Remove duplicate log:
  Removed the "UDP echo listener started" log from main.go since
  StartUDPListener already logs it. (skamboj on main.go:191)

Prometheus base units (seconds):
  Renamed goldpinger_peers_udp_rtt_ms back to goldpinger_peers_udp_rtt_s
  with sub-millisecond histogram buckets (.0001s to 1s), per Prometheus
  naming conventions. RTT is computed in seconds internally and only
  converted to ms for the JSON API. (skamboj on stats.go:150)

Rename path_length to hop_count:
  goldpinger_peers_path_length → goldpinger_peers_hop_count, and
  SetPeerPathLength → SetPeerHopCount. (skamboj on stats.go:139)

UDP buffer constant and packet size clamping:
  Added udpMaxPacketSize=1500 constant, documented as standard Ethernet
  MTU — the largest UDP payload that survives most networks without
  fragmentation. Used for both listener and prober receive buffers.
  ProbeUDP now clamps UDP_PACKET_SIZE to udpMaxPacketSize to prevent
  silent truncation if someone configures a size > MTU.
  (skamboj on udp_probe.go:54)

Guard count=0:
  ProbeUDP returns an error immediately if count <= 0 instead of
  dividing by zero. (skamboj on udp_probe.go:176)

UDP error counter:
  Added goldpinger_udp_errors_total counter (labels: goldpinger_instance,
  host). CountUDPError is called on dial failures and send errors.
  (skamboj on udp_probe.go:115)

Test: random source port for full loss:
  TestProbeUDP_FullLoss now binds an ephemeral port and closes it,
  instead of assuming port 19999 is free. (skamboj on udp_probe_test.go:56)

Test: partial loss validation:
  New TestProbeUDP_PartialLoss uses a lossy echo listener that drops
  every Nth packet to validate loss calculations are exact:
    drop every 2nd → 50.0%, every 3rd → 33.3%,
    every 5th → 20.0%, every 10th → 10.0%
  (skamboj on udp_probe_test.go:96)

Test: zero count:
  New TestProbeUDP_ZeroCount verifies error is returned for count=0.

Test results:
```
=== RUN   TestProbeUDP_NoLoss
    udp_probe_test.go:88: avg UDP RTT: 0.0816 ms
--- PASS: TestProbeUDP_NoLoss (0.00s)
=== RUN   TestProbeUDP_FullLoss
--- PASS: TestProbeUDP_FullLoss (0.00s)
=== RUN   TestProbeUDP_PartialLoss
=== RUN   TestProbeUDP_PartialLoss/drop_every_2nd_(50%)
    udp_probe_test.go:134: loss: 50.0% (expected 50.0%)
=== RUN   TestProbeUDP_PartialLoss/drop_every_3rd_(33.3%)
    udp_probe_test.go:134: loss: 33.3% (expected 33.3%)
=== RUN   TestProbeUDP_PartialLoss/drop_every_5th_(20%)
    udp_probe_test.go:134: loss: 20.0% (expected 20.0%)
=== RUN   TestProbeUDP_PartialLoss/drop_every_10th_(10%)
    udp_probe_test.go:134: loss: 10.0% (expected 10.0%)
--- PASS: TestProbeUDP_PartialLoss (8.00s)
=== RUN   TestProbeUDP_ZeroCount
--- PASS: TestProbeUDP_ZeroCount (0.00s)
=== RUN   TestProbeUDP_PacketFormat
--- PASS: TestProbeUDP_PacketFormat (0.00s)
=== RUN   TestEstimateHops
--- PASS: TestEstimateHops (0.00s)
PASS
```

Signed-off-by: Cooper Ry Lees <me@cooperlees.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:37:52 +00:00
Cooper Ry Lees
832bc7b598 Add UDP probe metrics: packet loss, hop count, and RTT
Add an opt-in UDP echo probe that runs alongside the existing HTTP
ping. Each goldpinger pod listens on a configurable UDP port (default
6969). During each ping cycle, the prober sends N sequenced packets
to the peer's listener, which echoes them back. From the replies we
compute packet loss percentage, path hop count (from IPv4 TTL / IPv6
HopLimit), and average round-trip time.

New Prometheus metrics:
  - goldpinger_peers_loss_pct      (gauge)     — per-peer UDP loss %
  - goldpinger_peers_path_length   (gauge)     — estimated hop count
  - goldpinger_peers_udp_rtt_ms    (histogram) — UDP RTT in milliseconds

The graph UI shows yellow edges for links with partial loss, and
displays sub-millisecond UDP RTT instead of HTTP latency when UDP
is enabled. Stale metric labels are cleaned up when a pinger is
destroyed so rolled pods don't leave ghost entries.

Configuration (all via env vars, disabled by default):
  UDP_ENABLED=true      enable UDP probing and listener
  UDP_PORT=6969         listener port
  UDP_PACKET_COUNT=10   packets per probe
  UDP_PACKET_SIZE=64    bytes per packet
  UDP_TIMEOUT=1s        probe timeout

New files:
  pkg/goldpinger/udp_probe.go       — echo listener + probe client
  pkg/goldpinger/udp_probe_test.go  — unit tests

Unit tests:
```
=== RUN   TestProbeUDP_NoLoss
    udp_probe_test.go:51: avg UDP RTT: 0.0823 ms
--- PASS: TestProbeUDP_NoLoss (0.00s)
=== RUN   TestProbeUDP_FullLoss
--- PASS: TestProbeUDP_FullLoss (0.00s)
=== RUN   TestProbeUDP_PacketFormat
--- PASS: TestProbeUDP_PacketFormat (0.00s)
=== RUN   TestEstimateHops
--- PASS: TestEstimateHops (0.00s)
PASS
```

Cluster test (6-node IPv6 k8s, UDP_ENABLED=true):
```
Prometheus metrics (healthy cluster, 0% loss):
  goldpinger_peers_loss_pct{...,pod_ip="fd00:4:69:3::3746"} 0
  goldpinger_peers_path_length{...,pod_ip="fd00:4:69:3::3746"} 0

Simulated 50% loss via ip6tables DROP in pod netns on node-0:
  goldpinger_peers_loss_pct{instance="server",...} 60
  goldpinger_peers_loss_pct{instance="node-1",...} 30
  goldpinger_peers_loss_pct{instance="server2",...} 30

UDP RTT vs HTTP RTT (check_all API):
  node-0 -> server:  udp=2.18ms  http=2ms
  node-2 -> node-2:  udp=0.40ms  http=1ms
  server -> node-0:  udp=0.55ms  http=2ms

Post-rollout stale metrics cleanup verified:
  All 36 edges show 0% loss, no stale pod IPs.
```

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Cooper Ry Lees <me@cooperlees.com>
2026-03-27 16:05:32 +00:00
63 changed files with 1347 additions and 375 deletions

View File

@@ -1,5 +1,5 @@
name ?= goldpinger
version ?= v3.10.3
version ?= v3.11.0
bin ?= goldpinger
pkg ?= "github.com/bloomberg/goldpinger"
tag = $(name):$(version)

View File

@@ -25,6 +25,7 @@ Oh, and it gives you the graph below for your cluster. Check out the [video expl
- [Authentication with Kubernetes API](#authentication-with-kubernetes-api)
- [Example YAML](#example-yaml)
- [Note on DNS](#note-on-dns)
- [UDP probe for packet loss, hop count, and RTT](#udp-probe-for-packet-loss-hop-count-and-rtt)
- [Usage](#usage)
- [UI](#ui)
- [API](#api)
@@ -287,10 +288,56 @@ Instances can also be configured to do simple TCP or HTTP checks on external tar
value: 10.34.5.141:5000 10.34.195.193:6442
```
the timeouts for the TCP, DNS and HTTP checks can be configured via `TCP_TARGETS_TIMEOUT`, `DNS_TARGETS_TIMEOUT` and `HTTP_TARGETS_TIMEOUT` respectively.
the timeouts for the TCP, DNS and HTTP checks can be configured via `TCP_TARGETS_TIMEOUT`, `DNS_TARGETS_TIMEOUT` and `HTTP_TARGETS_TIMEOUT` respectively.
![screenshot-tcp-http-checks](./extras/tcp-checks-screenshot.png)
### UDP probe for packet loss, hop count, and RTT
In natively routed Kubernetes environments (e.g. Cilium, Calico in BGP mode), the existing HTTP ping can mask network issues: TCP retransmits hide packet loss, and HTTP latency includes the 3-way handshake, TLS, and application overhead. The UDP probe gives you visibility into the actual network layer.
When enabled, each goldpinger pod runs a UDP echo listener. During each ping cycle, the prober sends a configurable number of sequenced UDP packets to each peer; the peer echoes them back. From the replies, goldpinger computes:
- **Packet loss** — percentage of packets that were not returned, surfacing degraded links before they impact applications
- **Hop count** — estimated from the IPv4 TTL or IPv6 HopLimit on received replies, useful for detecting asymmetric routing or unexpected topology changes
- **UDP RTT** — average round-trip time with sub-millisecond precision, isolating network latency from TCP/HTTP overhead
The feature is disabled by default and can be enabled with the following environment variables:
```sh
UDP_ENABLED=true # enable UDP probing and echo listener
UDP_PORT=6969 # listener port (default: 6969)
UDP_PACKET_COUNT=10 # packets per probe (default: 10)
UDP_PACKET_SIZE=64 # bytes per packet (default: 64)
UDP_TIMEOUT=1s # probe timeout (default: 1s)
```
Or via the Helm chart:
```yaml
goldpinger:
udp:
enabled: true
port: 6969
```
This adds three Prometheus metrics:
```sh
goldpinger_peers_loss_pct # gauge: UDP packet loss percentage (0-100)
goldpinger_peers_hop_count # gauge: estimated hop count
goldpinger_peers_udp_rtt_s # histogram: UDP round-trip time in seconds
goldpinger_udp_errors_total # counter: UDP probe errors
```
Links with partial loss are shown as yellow edges in the graph UI, and edge labels display the UDP RTT instead of HTTP latency when available.
![screenshot-udp-yellow-edges](./extras/udp-yellow-edges.png)
![screenshot-udp-grafana](./extras/udp-grafana-dashboards.png)
No new dependencies are required (`golang.org/x/net` is already in go.mod), and no additional container capabilities are needed.
## Usage
### UI
@@ -317,10 +364,12 @@ These are probably the droids you are looking for:
```sh
goldpinger_peers_response_time_s_*
goldpinger_peers_response_time_s_*
goldpinger_nodes_health_total
goldpinger_stats_total
goldpinger_errors_total
goldpinger_peers_loss_pct # (UDP probe, when enabled)
goldpinger_peers_hop_count # (UDP probe, when enabled)
goldpinger_peers_udp_rtt_s_* # (UDP probe, when enabled)
```
### Grafana

View File

@@ -1,7 +1,7 @@
apiVersion: v1
name: goldpinger
appVersion: "3.10.3"
version: 1.0.2
appVersion: "3.11.0"
version: 1.1.0
description: Goldpinger is a tool to help debug, troubleshoot and visualize network connectivity and slowness issues.
home: https://github.com/bloomberg/goldpinger
sources:

View File

@@ -50,6 +50,12 @@ spec:
value: "{{ .Values.goldpinger.port }}"
- name: LABEL_SELECTOR
value: "app.kubernetes.io/name={{ include "goldpinger.name" . }}"
{{- if .Values.goldpinger.udp.enabled }}
- name: UDP_ENABLED
value: "true"
- name: UDP_PORT
value: "{{ .Values.goldpinger.udp.port }}"
{{- end }}
{{- if .Values.extraEnv -}}
{{ toYaml .Values.extraEnv | nindent 12 }}
{{- end }}
@@ -66,6 +72,11 @@ spec:
hostPort: {{ $.Values.goldpinger.port }}
{{- end }}
{{- end }}
{{- if .Values.goldpinger.udp.enabled }}
- name: udp-probe
containerPort: {{ .Values.goldpinger.udp.port }}
protocol: UDP
{{- end }}
livenessProbe:
httpGet:
path: /

View File

@@ -18,6 +18,12 @@ spec:
targetPort: {{ .Values.goldpinger.port }}
protocol: TCP
name: http
{{- if .Values.goldpinger.udp.enabled }}
- port: {{ .Values.goldpinger.udp.port }}
targetPort: {{ .Values.goldpinger.udp.port }}
protocol: UDP
name: udp-probe
{{- end }}
selector:
{{- include "goldpinger.selectorLabels" . | nindent 4 }}
{{- if .Values.service.loadBalancerSourceRanges }}

View File

@@ -23,6 +23,9 @@ serviceAccount:
goldpinger:
port: 8080
udp:
enabled: false
port: 6969
zapConfig: |
{
"level": "info",

View File

@@ -186,6 +186,10 @@ func main() {
server.ConfigureAPI()
goldpinger.StartUpdater()
if goldpinger.GoldpingerConfig.UDPEnabled {
go goldpinger.StartUDPListener(goldpinger.GoldpingerConfig.UDPPort)
}
logger.Info("All good, starting serving the API")
// serve API

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

BIN
extras/udp-yellow-edges.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

View File

@@ -2,14 +2,12 @@
package client
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/bloomberg/goldpinger/v3/pkg/client/operations"
"github.com/go-openapi/runtime"
httptransport "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
"github.com/bloomberg/goldpinger/v3/pkg/client/operations"
)
// Default goldpinger HTTP client.

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"net/http"
@@ -52,10 +49,12 @@ func NewCheckAllPodsParamsWithHTTPClient(client *http.Client) *CheckAllPodsParam
}
}
/* CheckAllPodsParams contains all the parameters to send to the API endpoint
for the check all pods operation.
/*
CheckAllPodsParams contains all the parameters to send to the API endpoint
Typically these are written to a http.Request.
for the check all pods operation.
Typically these are written to a http.Request.
*/
type CheckAllPodsParams struct {
timeout time.Duration

View File

@@ -2,10 +2,9 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"encoding/json"
stderrors "errors"
"fmt"
"io"
@@ -21,7 +20,7 @@ type CheckAllPodsReader struct {
}
// ReadResponse reads a server response into the received o.
func (o *CheckAllPodsReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
func (o *CheckAllPodsReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
switch response.Code() {
case 200:
result := NewCheckAllPodsOK()
@@ -30,7 +29,7 @@ func (o *CheckAllPodsReader) ReadResponse(response runtime.ClientResponse, consu
}
return result, nil
default:
return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
return nil, runtime.NewAPIError("[GET /check_all] checkAllPods", response, response.Code())
}
}
@@ -39,7 +38,8 @@ func NewCheckAllPodsOK() *CheckAllPodsOK {
return &CheckAllPodsOK{}
}
/* CheckAllPodsOK describes a response with status code 200, with default header values.
/*
CheckAllPodsOK describes a response with status code 200, with default header values.
Success, return response
*/
@@ -47,9 +47,46 @@ type CheckAllPodsOK struct {
Payload *models.CheckAllResults
}
func (o *CheckAllPodsOK) Error() string {
return fmt.Sprintf("[GET /check_all][%d] checkAllPodsOK %+v", 200, o.Payload)
// IsSuccess returns true when this check all pods o k response has a 2xx status code
func (o *CheckAllPodsOK) IsSuccess() bool {
return true
}
// IsRedirect returns true when this check all pods o k response has a 3xx status code
func (o *CheckAllPodsOK) IsRedirect() bool {
return false
}
// IsClientError returns true when this check all pods o k response has a 4xx status code
func (o *CheckAllPodsOK) IsClientError() bool {
return false
}
// IsServerError returns true when this check all pods o k response has a 5xx status code
func (o *CheckAllPodsOK) IsServerError() bool {
return false
}
// IsCode returns true when this check all pods o k response a status code equal to that given
func (o *CheckAllPodsOK) IsCode(code int) bool {
return code == 200
}
// Code gets the status code for the check all pods o k response
func (o *CheckAllPodsOK) Code() int {
return 200
}
func (o *CheckAllPodsOK) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[GET /check_all][%d] checkAllPodsOK %s", 200, payload)
}
func (o *CheckAllPodsOK) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[GET /check_all][%d] checkAllPodsOK %s", 200, payload)
}
func (o *CheckAllPodsOK) GetPayload() *models.CheckAllResults {
return o.Payload
}
@@ -59,7 +96,7 @@ func (o *CheckAllPodsOK) readResponse(response runtime.ClientResponse, consumer
o.Payload = new(models.CheckAllResults)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) {
return err
}

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"net/http"
@@ -52,10 +49,12 @@ func NewCheckServicePodsParamsWithHTTPClient(client *http.Client) *CheckServiceP
}
}
/* CheckServicePodsParams contains all the parameters to send to the API endpoint
for the check service pods operation.
/*
CheckServicePodsParams contains all the parameters to send to the API endpoint
Typically these are written to a http.Request.
for the check service pods operation.
Typically these are written to a http.Request.
*/
type CheckServicePodsParams struct {
timeout time.Duration

View File

@@ -2,10 +2,9 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"encoding/json"
stderrors "errors"
"fmt"
"io"
@@ -21,7 +20,7 @@ type CheckServicePodsReader struct {
}
// ReadResponse reads a server response into the received o.
func (o *CheckServicePodsReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
func (o *CheckServicePodsReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
switch response.Code() {
case 200:
result := NewCheckServicePodsOK()
@@ -30,7 +29,7 @@ func (o *CheckServicePodsReader) ReadResponse(response runtime.ClientResponse, c
}
return result, nil
default:
return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
return nil, runtime.NewAPIError("[GET /check] checkServicePods", response, response.Code())
}
}
@@ -39,7 +38,8 @@ func NewCheckServicePodsOK() *CheckServicePodsOK {
return &CheckServicePodsOK{}
}
/* CheckServicePodsOK describes a response with status code 200, with default header values.
/*
CheckServicePodsOK describes a response with status code 200, with default header values.
Success, return response
*/
@@ -47,9 +47,46 @@ type CheckServicePodsOK struct {
Payload *models.CheckResults
}
func (o *CheckServicePodsOK) Error() string {
return fmt.Sprintf("[GET /check][%d] checkServicePodsOK %+v", 200, o.Payload)
// IsSuccess returns true when this check service pods o k response has a 2xx status code
func (o *CheckServicePodsOK) IsSuccess() bool {
return true
}
// IsRedirect returns true when this check service pods o k response has a 3xx status code
func (o *CheckServicePodsOK) IsRedirect() bool {
return false
}
// IsClientError returns true when this check service pods o k response has a 4xx status code
func (o *CheckServicePodsOK) IsClientError() bool {
return false
}
// IsServerError returns true when this check service pods o k response has a 5xx status code
func (o *CheckServicePodsOK) IsServerError() bool {
return false
}
// IsCode returns true when this check service pods o k response a status code equal to that given
func (o *CheckServicePodsOK) IsCode(code int) bool {
return code == 200
}
// Code gets the status code for the check service pods o k response
func (o *CheckServicePodsOK) Code() int {
return 200
}
func (o *CheckServicePodsOK) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[GET /check][%d] checkServicePodsOK %s", 200, payload)
}
func (o *CheckServicePodsOK) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[GET /check][%d] checkServicePodsOK %s", 200, payload)
}
func (o *CheckServicePodsOK) GetPayload() *models.CheckResults {
return o.Payload
}
@@ -59,7 +96,7 @@ func (o *CheckServicePodsOK) readResponse(response runtime.ClientResponse, consu
o.Payload = new(models.CheckResults)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) {
return err
}

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"net/http"
@@ -52,10 +49,12 @@ func NewClusterHealthParamsWithHTTPClient(client *http.Client) *ClusterHealthPar
}
}
/* ClusterHealthParams contains all the parameters to send to the API endpoint
for the cluster health operation.
/*
ClusterHealthParams contains all the parameters to send to the API endpoint
Typically these are written to a http.Request.
for the cluster health operation.
Typically these are written to a http.Request.
*/
type ClusterHealthParams struct {
timeout time.Duration

View File

@@ -2,10 +2,9 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"encoding/json"
stderrors "errors"
"fmt"
"io"
@@ -21,7 +20,7 @@ type ClusterHealthReader struct {
}
// ReadResponse reads a server response into the received o.
func (o *ClusterHealthReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
func (o *ClusterHealthReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
switch response.Code() {
case 200:
result := NewClusterHealthOK()
@@ -36,7 +35,7 @@ func (o *ClusterHealthReader) ReadResponse(response runtime.ClientResponse, cons
}
return nil, result
default:
return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
return nil, runtime.NewAPIError("[GET /cluster_health] clusterHealth", response, response.Code())
}
}
@@ -45,7 +44,8 @@ func NewClusterHealthOK() *ClusterHealthOK {
return &ClusterHealthOK{}
}
/* ClusterHealthOK describes a response with status code 200, with default header values.
/*
ClusterHealthOK describes a response with status code 200, with default header values.
Healthy cluster
*/
@@ -53,9 +53,46 @@ type ClusterHealthOK struct {
Payload *models.ClusterHealthResults
}
func (o *ClusterHealthOK) Error() string {
return fmt.Sprintf("[GET /cluster_health][%d] clusterHealthOK %+v", 200, o.Payload)
// IsSuccess returns true when this cluster health o k response has a 2xx status code
func (o *ClusterHealthOK) IsSuccess() bool {
return true
}
// IsRedirect returns true when this cluster health o k response has a 3xx status code
func (o *ClusterHealthOK) IsRedirect() bool {
return false
}
// IsClientError returns true when this cluster health o k response has a 4xx status code
func (o *ClusterHealthOK) IsClientError() bool {
return false
}
// IsServerError returns true when this cluster health o k response has a 5xx status code
func (o *ClusterHealthOK) IsServerError() bool {
return false
}
// IsCode returns true when this cluster health o k response a status code equal to that given
func (o *ClusterHealthOK) IsCode(code int) bool {
return code == 200
}
// Code gets the status code for the cluster health o k response
func (o *ClusterHealthOK) Code() int {
return 200
}
func (o *ClusterHealthOK) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[GET /cluster_health][%d] clusterHealthOK %s", 200, payload)
}
func (o *ClusterHealthOK) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[GET /cluster_health][%d] clusterHealthOK %s", 200, payload)
}
func (o *ClusterHealthOK) GetPayload() *models.ClusterHealthResults {
return o.Payload
}
@@ -65,7 +102,7 @@ func (o *ClusterHealthOK) readResponse(response runtime.ClientResponse, consumer
o.Payload = new(models.ClusterHealthResults)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) {
return err
}
@@ -77,7 +114,8 @@ func NewClusterHealthIMATeapot() *ClusterHealthIMATeapot {
return &ClusterHealthIMATeapot{}
}
/* ClusterHealthIMATeapot describes a response with status code 418, with default header values.
/*
ClusterHealthIMATeapot describes a response with status code 418, with default header values.
Unhealthy cluster
*/
@@ -85,9 +123,46 @@ type ClusterHealthIMATeapot struct {
Payload *models.ClusterHealthResults
}
func (o *ClusterHealthIMATeapot) Error() string {
return fmt.Sprintf("[GET /cluster_health][%d] clusterHealthIMATeapot %+v", 418, o.Payload)
// IsSuccess returns true when this cluster health i m a teapot response has a 2xx status code
func (o *ClusterHealthIMATeapot) IsSuccess() bool {
return false
}
// IsRedirect returns true when this cluster health i m a teapot response has a 3xx status code
func (o *ClusterHealthIMATeapot) IsRedirect() bool {
return false
}
// IsClientError returns true when this cluster health i m a teapot response has a 4xx status code
func (o *ClusterHealthIMATeapot) IsClientError() bool {
return true
}
// IsServerError returns true when this cluster health i m a teapot response has a 5xx status code
func (o *ClusterHealthIMATeapot) IsServerError() bool {
return false
}
// IsCode returns true when this cluster health i m a teapot response a status code equal to that given
func (o *ClusterHealthIMATeapot) IsCode(code int) bool {
return code == 418
}
// Code gets the status code for the cluster health i m a teapot response
func (o *ClusterHealthIMATeapot) Code() int {
return 418
}
func (o *ClusterHealthIMATeapot) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[GET /cluster_health][%d] clusterHealthIMATeapot %s", 418, payload)
}
func (o *ClusterHealthIMATeapot) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[GET /cluster_health][%d] clusterHealthIMATeapot %s", 418, payload)
}
func (o *ClusterHealthIMATeapot) GetPayload() *models.ClusterHealthResults {
return o.Payload
}
@@ -97,7 +172,7 @@ func (o *ClusterHealthIMATeapot) readResponse(response runtime.ClientResponse, c
o.Payload = new(models.ClusterHealthResults)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) {
return err
}

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"net/http"
@@ -52,10 +49,12 @@ func NewHealthzParamsWithHTTPClient(client *http.Client) *HealthzParams {
}
}
/* HealthzParams contains all the parameters to send to the API endpoint
for the healthz operation.
/*
HealthzParams contains all the parameters to send to the API endpoint
Typically these are written to a http.Request.
for the healthz operation.
Typically these are written to a http.Request.
*/
type HealthzParams struct {
timeout time.Duration

View File

@@ -2,10 +2,9 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"encoding/json"
stderrors "errors"
"fmt"
"io"
@@ -21,7 +20,7 @@ type HealthzReader struct {
}
// ReadResponse reads a server response into the received o.
func (o *HealthzReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
func (o *HealthzReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
switch response.Code() {
case 200:
result := NewHealthzOK()
@@ -36,7 +35,7 @@ func (o *HealthzReader) ReadResponse(response runtime.ClientResponse, consumer r
}
return nil, result
default:
return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
return nil, runtime.NewAPIError("[GET /healthz] healthz", response, response.Code())
}
}
@@ -45,7 +44,8 @@ func NewHealthzOK() *HealthzOK {
return &HealthzOK{}
}
/* HealthzOK describes a response with status code 200, with default header values.
/*
HealthzOK describes a response with status code 200, with default header values.
Health check report
*/
@@ -53,9 +53,46 @@ type HealthzOK struct {
Payload *models.HealthCheckResults
}
func (o *HealthzOK) Error() string {
return fmt.Sprintf("[GET /healthz][%d] healthzOK %+v", 200, o.Payload)
// IsSuccess returns true when this healthz o k response has a 2xx status code
func (o *HealthzOK) IsSuccess() bool {
return true
}
// IsRedirect returns true when this healthz o k response has a 3xx status code
func (o *HealthzOK) IsRedirect() bool {
return false
}
// IsClientError returns true when this healthz o k response has a 4xx status code
func (o *HealthzOK) IsClientError() bool {
return false
}
// IsServerError returns true when this healthz o k response has a 5xx status code
func (o *HealthzOK) IsServerError() bool {
return false
}
// IsCode returns true when this healthz o k response a status code equal to that given
func (o *HealthzOK) IsCode(code int) bool {
return code == 200
}
// Code gets the status code for the healthz o k response
func (o *HealthzOK) Code() int {
return 200
}
func (o *HealthzOK) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[GET /healthz][%d] healthzOK %s", 200, payload)
}
func (o *HealthzOK) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[GET /healthz][%d] healthzOK %s", 200, payload)
}
func (o *HealthzOK) GetPayload() *models.HealthCheckResults {
return o.Payload
}
@@ -65,7 +102,7 @@ func (o *HealthzOK) readResponse(response runtime.ClientResponse, consumer runti
o.Payload = new(models.HealthCheckResults)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) {
return err
}
@@ -77,7 +114,8 @@ func NewHealthzServiceUnavailable() *HealthzServiceUnavailable {
return &HealthzServiceUnavailable{}
}
/* HealthzServiceUnavailable describes a response with status code 503, with default header values.
/*
HealthzServiceUnavailable describes a response with status code 503, with default header values.
Unhealthy service
*/
@@ -85,9 +123,46 @@ type HealthzServiceUnavailable struct {
Payload *models.HealthCheckResults
}
func (o *HealthzServiceUnavailable) Error() string {
return fmt.Sprintf("[GET /healthz][%d] healthzServiceUnavailable %+v", 503, o.Payload)
// IsSuccess returns true when this healthz service unavailable response has a 2xx status code
func (o *HealthzServiceUnavailable) IsSuccess() bool {
return false
}
// IsRedirect returns true when this healthz service unavailable response has a 3xx status code
func (o *HealthzServiceUnavailable) IsRedirect() bool {
return false
}
// IsClientError returns true when this healthz service unavailable response has a 4xx status code
func (o *HealthzServiceUnavailable) IsClientError() bool {
return false
}
// IsServerError returns true when this healthz service unavailable response has a 5xx status code
func (o *HealthzServiceUnavailable) IsServerError() bool {
return true
}
// IsCode returns true when this healthz service unavailable response a status code equal to that given
func (o *HealthzServiceUnavailable) IsCode(code int) bool {
return code == 503
}
// Code gets the status code for the healthz service unavailable response
func (o *HealthzServiceUnavailable) Code() int {
return 503
}
func (o *HealthzServiceUnavailable) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[GET /healthz][%d] healthzServiceUnavailable %s", 503, payload)
}
func (o *HealthzServiceUnavailable) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[GET /healthz][%d] healthzServiceUnavailable %s", 503, payload)
}
func (o *HealthzServiceUnavailable) GetPayload() *models.HealthCheckResults {
return o.Payload
}
@@ -97,7 +172,7 @@ func (o *HealthzServiceUnavailable) readResponse(response runtime.ClientResponse
o.Payload = new(models.HealthCheckResults)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) {
return err
}

View File

@@ -2,13 +2,11 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"fmt"
"github.com/go-openapi/runtime"
httptransport "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
)
@@ -17,6 +15,31 @@ func New(transport runtime.ClientTransport, formats strfmt.Registry) ClientServi
return &Client{transport: transport, formats: formats}
}
// New creates a new operations API client with basic auth credentials.
// It takes the following parameters:
// - host: http host (github.com).
// - basePath: any base path for the API client ("/v1", "/v3").
// - scheme: http scheme ("http", "https").
// - user: user for basic authentication header.
// - password: password for basic authentication header.
func NewClientWithBasicAuth(host, basePath, scheme, user, password string) ClientService {
transport := httptransport.New(host, basePath, []string{scheme})
transport.DefaultAuthentication = httptransport.BasicAuth(user, password)
return &Client{transport: transport, formats: strfmt.Default}
}
// New creates a new operations API client with a bearer token for authentication.
// It takes the following parameters:
// - host: http host (github.com).
// - basePath: any base path for the API client ("/v1", "/v3").
// - scheme: http scheme ("http", "https").
// - bearerToken: bearer token for Bearer authentication header.
func NewClientWithBearerToken(host, basePath, scheme, bearerToken string) ClientService {
transport := httptransport.New(host, basePath, []string{scheme})
transport.DefaultAuthentication = httptransport.BearerToken(bearerToken)
return &Client{transport: transport, formats: strfmt.Default}
}
/*
Client for operations API
*/
@@ -25,7 +48,7 @@ type Client struct {
formats strfmt.Registry
}
// ClientOption is the option for Client methods
// ClientOption may be used to customize the behavior of Client methods.
type ClientOption func(*runtime.ClientOperation)
// ClientService is the interface for Client methods
@@ -44,10 +67,10 @@ type ClientService interface {
}
/*
CheckAllPods Queries the API server for all other pods in this service, and makes all of them query all of their neighbours, using their pods IPs. Calls their /check endpoint.
CheckAllPods Queries the API server for all other pods in this service, and makes all of them query all of their neighbours, using their pods IPs. Calls their /check endpoint.
*/
func (a *Client) CheckAllPods(params *CheckAllPodsParams, opts ...ClientOption) (*CheckAllPodsOK, error) {
// TODO: Validate the params before sending
// NOTE: parameters are not validated before sending
if params == nil {
params = NewCheckAllPodsParams()
}
@@ -66,26 +89,31 @@ func (a *Client) CheckAllPods(params *CheckAllPodsParams, opts ...ClientOption)
for _, opt := range opts {
opt(op)
}
result, err := a.transport.Submit(op)
if err != nil {
return nil, err
}
// only one success response has to be checked
success, ok := result.(*CheckAllPodsOK)
if ok {
return success, nil
}
// unexpected success response
// safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue
// unexpected success response.
// no default response is defined.
//
// safeguard: normally, in the absence of a default response, unknown success responses return an error above: so this is a codegen issue
msg := fmt.Sprintf("unexpected success response for checkAllPods: API contract not enforced by server. Client expected to get an error, but got: %T", result)
panic(msg)
}
/*
CheckServicePods Queries the API server for all other pods in this service, and pings them via their pods IPs. Calls their /ping endpoint
CheckServicePods Queries the API server for all other pods in this service, and pings them via their pods IPs. Calls their /ping endpoint
*/
func (a *Client) CheckServicePods(params *CheckServicePodsParams, opts ...ClientOption) (*CheckServicePodsOK, error) {
// TODO: Validate the params before sending
// NOTE: parameters are not validated before sending
if params == nil {
params = NewCheckServicePodsParams()
}
@@ -104,26 +132,31 @@ func (a *Client) CheckServicePods(params *CheckServicePodsParams, opts ...Client
for _, opt := range opts {
opt(op)
}
result, err := a.transport.Submit(op)
if err != nil {
return nil, err
}
// only one success response has to be checked
success, ok := result.(*CheckServicePodsOK)
if ok {
return success, nil
}
// unexpected success response
// safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue
// unexpected success response.
// no default response is defined.
//
// safeguard: normally, in the absence of a default response, unknown success responses return an error above: so this is a codegen issue
msg := fmt.Sprintf("unexpected success response for checkServicePods: API contract not enforced by server. Client expected to get an error, but got: %T", result)
panic(msg)
}
/*
ClusterHealth Checks the full graph. Returns a binary OK or not OK.
ClusterHealth Checks the full graph. Returns a binary OK or not OK.
*/
func (a *Client) ClusterHealth(params *ClusterHealthParams, opts ...ClientOption) (*ClusterHealthOK, error) {
// TODO: Validate the params before sending
// NOTE: parameters are not validated before sending
if params == nil {
params = NewClusterHealthParams()
}
@@ -142,26 +175,31 @@ func (a *Client) ClusterHealth(params *ClusterHealthParams, opts ...ClientOption
for _, opt := range opts {
opt(op)
}
result, err := a.transport.Submit(op)
if err != nil {
return nil, err
}
// only one success response has to be checked
success, ok := result.(*ClusterHealthOK)
if ok {
return success, nil
}
// unexpected success response
// safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue
// unexpected success response.
// no default response is defined.
//
// safeguard: normally, in the absence of a default response, unknown success responses return an error above: so this is a codegen issue
msg := fmt.Sprintf("unexpected success response for clusterHealth: API contract not enforced by server. Client expected to get an error, but got: %T", result)
panic(msg)
}
/*
Healthz The healthcheck endpoint provides detailed information about the health of a web service. If each of the components required by the service are healthy, then the service is considered healthy and will return a 200 OK response. If any of the components needed by the service are unhealthy, then a 503 Service Unavailable response will be provided.
Healthz The healthcheck endpoint provides detailed information about the health of a web service. If each of the components required by the service are healthy, then the service is considered healthy and will return a 200 OK response. If any of the components needed by the service are unhealthy, then a 503 Service Unavailable response will be provided.
*/
func (a *Client) Healthz(params *HealthzParams, opts ...ClientOption) (*HealthzOK, error) {
// TODO: Validate the params before sending
// NOTE: parameters are not validated before sending
if params == nil {
params = NewHealthzParams()
}
@@ -180,26 +218,31 @@ func (a *Client) Healthz(params *HealthzParams, opts ...ClientOption) (*HealthzO
for _, opt := range opts {
opt(op)
}
result, err := a.transport.Submit(op)
if err != nil {
return nil, err
}
// only one success response has to be checked
success, ok := result.(*HealthzOK)
if ok {
return success, nil
}
// unexpected success response
// safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue
// unexpected success response.
// no default response is defined.
//
// safeguard: normally, in the absence of a default response, unknown success responses return an error above: so this is a codegen issue
msg := fmt.Sprintf("unexpected success response for healthz: API contract not enforced by server. Client expected to get an error, but got: %T", result)
panic(msg)
}
/*
Ping return query stats
Ping return query stats
*/
func (a *Client) Ping(params *PingParams, opts ...ClientOption) (*PingOK, error) {
// TODO: Validate the params before sending
// NOTE: parameters are not validated before sending
if params == nil {
params = NewPingParams()
}
@@ -218,17 +261,22 @@ func (a *Client) Ping(params *PingParams, opts ...ClientOption) (*PingOK, error)
for _, opt := range opts {
opt(op)
}
result, err := a.transport.Submit(op)
if err != nil {
return nil, err
}
// only one success response has to be checked
success, ok := result.(*PingOK)
if ok {
return success, nil
}
// unexpected success response
// safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue
// unexpected success response.
// no default response is defined.
//
// safeguard: normally, in the absence of a default response, unknown success responses return an error above: so this is a codegen issue
msg := fmt.Sprintf("unexpected success response for ping: API contract not enforced by server. Client expected to get an error, but got: %T", result)
panic(msg)
}

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"net/http"
@@ -52,10 +49,12 @@ func NewPingParamsWithHTTPClient(client *http.Client) *PingParams {
}
}
/* PingParams contains all the parameters to send to the API endpoint
for the ping operation.
/*
PingParams contains all the parameters to send to the API endpoint
Typically these are written to a http.Request.
for the ping operation.
Typically these are written to a http.Request.
*/
type PingParams struct {
timeout time.Duration

View File

@@ -2,10 +2,9 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"encoding/json"
stderrors "errors"
"fmt"
"io"
@@ -21,7 +20,7 @@ type PingReader struct {
}
// ReadResponse reads a server response into the received o.
func (o *PingReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
func (o *PingReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
switch response.Code() {
case 200:
result := NewPingOK()
@@ -30,7 +29,7 @@ func (o *PingReader) ReadResponse(response runtime.ClientResponse, consumer runt
}
return result, nil
default:
return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
return nil, runtime.NewAPIError("[GET /ping] ping", response, response.Code())
}
}
@@ -39,7 +38,8 @@ func NewPingOK() *PingOK {
return &PingOK{}
}
/* PingOK describes a response with status code 200, with default header values.
/*
PingOK describes a response with status code 200, with default header values.
return success
*/
@@ -47,9 +47,46 @@ type PingOK struct {
Payload *models.PingResults
}
func (o *PingOK) Error() string {
return fmt.Sprintf("[GET /ping][%d] pingOK %+v", 200, o.Payload)
// IsSuccess returns true when this ping o k response has a 2xx status code
func (o *PingOK) IsSuccess() bool {
return true
}
// IsRedirect returns true when this ping o k response has a 3xx status code
func (o *PingOK) IsRedirect() bool {
return false
}
// IsClientError returns true when this ping o k response has a 4xx status code
func (o *PingOK) IsClientError() bool {
return false
}
// IsServerError returns true when this ping o k response has a 5xx status code
func (o *PingOK) IsServerError() bool {
return false
}
// IsCode returns true when this ping o k response a status code equal to that given
func (o *PingOK) IsCode(code int) bool {
return code == 200
}
// Code gets the status code for the ping o k response
func (o *PingOK) Code() int {
return 200
}
func (o *PingOK) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[GET /ping][%d] pingOK %s", 200, payload)
}
func (o *PingOK) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[GET /ping][%d] pingOK %s", 200, payload)
}
func (o *PingOK) GetPayload() *models.PingResults {
return o.Payload
}
@@ -59,7 +96,7 @@ func (o *PingOK) readResponse(response runtime.ClientResponse, consumer runtime.
o.Payload = new(models.PingResults)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) {
return err
}

View File

@@ -44,6 +44,13 @@ var GoldpingerConfig = struct {
IPVersions []string `long:"ip-versions" description:"The IP versions to use (space delimited). Possible values are 4 and 6 (defaults to 4)." env:"IP_VERSIONS" env-delim:" "`
// UDP probe settings
UDPEnabled bool `long:"udp-enabled" description:"Enable UDP probe for loss and hop metrics" env:"UDP_ENABLED"`
UDPPort int `long:"udp-port" description:"Port for UDP echo listener" env:"UDP_PORT" default:"6969"`
UDPPacketCount int `long:"udp-packet-count" description:"Number of UDP packets to send per probe" env:"UDP_PACKET_COUNT" default:"10"`
UDPPacketSize int `long:"udp-packet-size" description:"Size of each UDP probe packet in bytes" env:"UDP_PACKET_SIZE" default:"64"`
UDPTimeout time.Duration `long:"udp-timeout" description:"Timeout for UDP probe" env:"UDP_TIMEOUT" default:"1s"`
// Timeouts
PingTimeoutMs int64 `long:"ping-timeout-ms" description:"The timeout in milliseconds for a ping call to other goldpinger pods(deprecated)" env:"PING_TIMEOUT_MS" default:"300"`
CheckTimeoutMs int64 `long:"check-timeout-ms" description:"The timeout in milliseconds for a check call to other goldpinger pods(deprecated)" env:"CHECK_TIMEOUT_MS" default:"1000"`

View File

@@ -16,6 +16,7 @@ package goldpinger
import (
"context"
"sync"
"time"
"go.uber.org/zap"
@@ -111,6 +112,24 @@ func (p *Pinger) Ping() {
CountCall("made", "ping")
start := time.Now()
// Run HTTP ping and UDP probe concurrently
var udpResult UDPProbeResult
var wg sync.WaitGroup
if GoldpingerConfig.UDPEnabled {
wg.Add(1)
go func() {
defer wg.Done()
targetIP := pickPodHostIP(p.pod.PodIP, p.pod.HostIP)
udpResult = ProbeUDP(
targetIP,
GoldpingerConfig.UDPPort,
GoldpingerConfig.UDPPacketCount,
GoldpingerConfig.UDPPacketSize,
GoldpingerConfig.UDPTimeout,
)
}()
}
ctx, cancel := context.WithTimeout(context.Background(), p.timeout)
defer cancel()
@@ -120,7 +139,34 @@ func (p *Pinger) Ping() {
responseTimeMs := responseTime.Nanoseconds() / int64(time.Millisecond)
p.histogram.Observe(responseTime.Seconds())
// Wait for UDP probe to complete
wg.Wait()
OK := (err == nil)
var lossPct float64
var hopCount int32
var udpRttMs float64
if GoldpingerConfig.UDPEnabled {
lossPct = udpResult.LossPct
hopCount = udpResult.HopCount
udpRttMs = udpResult.AvgRttS * 1000.0
if udpResult.Err != nil {
p.logger.Warn("UDP probe error", zap.Error(udpResult.Err))
} else {
p.logger.Debug("UDP probe complete",
zap.Float64("lossPct", lossPct),
zap.Int32("hopCount", hopCount),
zap.Float64("udpRttMs", udpRttMs),
)
}
SetPeerLossPct(p.pod.HostIP, p.pod.PodIP, lossPct)
SetPeerHopCount(p.pod.HostIP, p.pod.PodIP, hopCount)
if udpResult.AvgRttS > 0 {
ObservePeerUDPRtt(p.pod.HostIP, p.pod.PodIP, udpResult.AvgRttS)
}
}
if OK {
p.resultsChan <- PingAllPodsResult{
podName: p.pod.Name,
@@ -132,6 +178,9 @@ func (p *Pinger) Ping() {
Response: resp.Payload,
StatusCode: 200,
ResponseTimeMs: responseTimeMs,
LossPct: lossPct,
HopCount: hopCount,
UDPRttMs: udpRttMs,
},
}
p.logger.Debug("Success pinging pod", zap.Duration("responseTime", responseTime))
@@ -146,6 +195,9 @@ func (p *Pinger) Ping() {
Error: err.Error(),
StatusCode: 504,
ResponseTimeMs: responseTimeMs,
LossPct: lossPct,
HopCount: hopCount,
UDPRttMs: udpRttMs,
},
}
p.logger.Warn("Ping returned error", zap.Duration("responseTime", responseTime), zap.Error(err))

View File

@@ -123,6 +123,50 @@ var (
"host",
},
)
goldpingerPeersLossPct = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "goldpinger_peers_loss_pct",
Help: "UDP packet loss percentage to peer (0-100)",
},
[]string{
"goldpinger_instance",
"host_ip",
"pod_ip",
},
)
goldpingerPeersHopCount = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "goldpinger_peers_hop_count",
Help: "Estimated network hop count to peer from UDP TTL",
},
[]string{
"goldpinger_instance",
"host_ip",
"pod_ip",
},
)
goldpingerPeersUDPRtt = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "goldpinger_peers_udp_rtt_s",
Help: "Histogram of UDP round-trip times to peers in seconds",
Buckets: []float64{.0001, .00025, .0005, .001, .0025, .005, .01, .025, .05, .1, .25, .5, 1},
},
[]string{
"goldpinger_instance",
"host_ip",
"pod_ip",
},
)
goldpingerUDPErrorsCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "goldpinger_udp_errors_total",
Help: "Statistics of UDP probe errors per instance",
},
[]string{
"goldpinger_instance",
"host",
},
)
bootTime = time.Now()
)
@@ -136,6 +180,10 @@ func init() {
prometheus.MustRegister(goldpingerDnsErrorsCounter)
prometheus.MustRegister(goldPingerHttpErrorsCounter)
prometheus.MustRegister(goldPingerTcpErrorsCounter)
prometheus.MustRegister(goldpingerPeersLossPct)
prometheus.MustRegister(goldpingerPeersHopCount)
prometheus.MustRegister(goldpingerPeersUDPRtt)
prometheus.MustRegister(goldpingerUDPErrorsCounter)
zap.L().Info("Metrics setup - see /metrics")
}
@@ -210,6 +258,48 @@ func CountHttpError(host string) {
).Inc()
}
// SetPeerLossPct sets the UDP packet loss percentage gauge for a peer
func SetPeerLossPct(hostIP, podIP string, lossPct float64) {
goldpingerPeersLossPct.WithLabelValues(
GoldpingerConfig.Hostname,
hostIP,
podIP,
).Set(lossPct)
}
// SetPeerHopCount sets the estimated hop count gauge for a peer
func SetPeerHopCount(hostIP, podIP string, hopCount int32) {
goldpingerPeersHopCount.WithLabelValues(
GoldpingerConfig.Hostname,
hostIP,
podIP,
).Set(float64(hopCount))
}
// DeletePeerUDPMetrics removes stale UDP metric labels for a destroyed peer
func DeletePeerUDPMetrics(hostIP, podIP string) {
goldpingerPeersLossPct.DeleteLabelValues(GoldpingerConfig.Hostname, hostIP, podIP)
goldpingerPeersHopCount.DeleteLabelValues(GoldpingerConfig.Hostname, hostIP, podIP)
goldpingerPeersUDPRtt.DeleteLabelValues(GoldpingerConfig.Hostname, hostIP, podIP)
}
// ObservePeerUDPRtt records a UDP RTT observation in seconds
func ObservePeerUDPRtt(hostIP, podIP string, rttS float64) {
goldpingerPeersUDPRtt.WithLabelValues(
GoldpingerConfig.Hostname,
hostIP,
podIP,
).Observe(rttS)
}
// CountUDPError counts instances of UDP probe errors
func CountUDPError(host string) {
goldpingerUDPErrorsCounter.WithLabelValues(
GoldpingerConfig.Hostname,
host,
).Inc()
}
// returns a timer for easy observing of the durations of calls to kubernetes API
func GetLabeledKubernetesCallsTimer() *prometheus.Timer {
return prometheus.NewTimer(

225
pkg/goldpinger/udp_probe.go Normal file
View File

@@ -0,0 +1,225 @@
// Copyright 2018 Bloomberg Finance L.P.
//
// Licensed 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.
package goldpinger
import (
"encoding/binary"
"fmt"
"net"
"strconv"
"time"
"go.uber.org/zap"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
)
const (
udpMagic = 0x47504E47 // "GPNG"
udpHeaderSize = 16 // 4 magic + 4 seq + 8 timestamp
// udpMaxPacketSize is the maximum UDP packet we will read. Sized to the
// standard Ethernet MTU (1500 bytes) which is the largest packet that
// will survive most networks without fragmentation. Any configured
// UDP_PACKET_SIZE larger than this will be clamped to this value to
// avoid silent truncation on the receive side.
udpMaxPacketSize = 1500
)
// UDPProbeResult holds the results of a UDP probe to a peer
type UDPProbeResult struct {
LossPct float64
HopCount int32
AvgRttS float64
Err error
}
// StartUDPListener starts a UDP echo listener on the given port.
// It echoes back any packet that starts with the GPNG magic number.
func StartUDPListener(port int) {
addr := net.JoinHostPort("::", strconv.Itoa(port))
pc, err := net.ListenPacket("udp", addr)
if err != nil {
zap.L().Fatal("Failed to start UDP listener", zap.String("addr", addr), zap.Error(err))
}
defer pc.Close()
zap.L().Info("UDP echo listener started", zap.String("addr", addr))
buf := make([]byte, udpMaxPacketSize)
for {
n, remoteAddr, err := pc.ReadFrom(buf)
if err != nil {
zap.L().Warn("UDP read error", zap.Error(err))
continue
}
if n < udpHeaderSize {
continue
}
magic := binary.BigEndian.Uint32(buf[0:4])
if magic != udpMagic {
continue
}
// Echo back the packet as-is
_, err = pc.WriteTo(buf[:n], remoteAddr)
if err != nil {
zap.L().Warn("UDP write error", zap.Error(err))
}
}
}
// ProbeUDP sends count UDP packets to the target and measures loss and hop count.
func ProbeUDP(targetIP string, port, count, size int, timeout time.Duration) UDPProbeResult {
if count <= 0 {
return UDPProbeResult{Err: fmt.Errorf("packet count must be > 0, got %d", count)}
}
if size < udpHeaderSize {
size = udpHeaderSize
}
if size > udpMaxPacketSize {
size = udpMaxPacketSize
}
addr := net.JoinHostPort(targetIP, strconv.Itoa(port))
conn, err := net.Dial("udp", addr)
if err != nil {
CountUDPError(targetIP)
return UDPProbeResult{LossPct: 100, Err: fmt.Errorf("dial: %w", err)}
}
defer conn.Close()
// Determine if this is IPv4 or IPv6 and set up TTL/HopLimit reading
isIPv6 := net.ParseIP(targetIP).To4() == nil
var ttlValue int
ttlFound := false
if isIPv6 {
p := ipv6.NewPacketConn(conn.(*net.UDPConn))
if err := p.SetControlMessage(ipv6.FlagHopLimit, true); err != nil {
zap.L().Debug("Cannot set IPv6 hop limit flag", zap.Error(err))
}
} else {
p := ipv4.NewPacketConn(conn.(*net.UDPConn))
if err := p.SetControlMessage(ipv4.FlagTTL, true); err != nil {
zap.L().Debug("Cannot set IPv4 TTL flag", zap.Error(err))
}
}
// Build packet
pkt := make([]byte, size)
binary.BigEndian.PutUint32(pkt[0:4], udpMagic)
// Send all packets
for i := 0; i < count; i++ {
binary.BigEndian.PutUint32(pkt[4:8], uint32(i))
binary.BigEndian.PutUint64(pkt[8:16], uint64(time.Now().UnixNano()))
_, err := conn.Write(pkt)
if err != nil {
CountUDPError(targetIP)
zap.L().Debug("UDP send error", zap.Int("seq", i), zap.Error(err))
}
}
// Receive replies
received := 0
var totalRttNs int64
deadline := time.Now().Add(timeout)
conn.SetReadDeadline(deadline)
recvBuf := make([]byte, udpMaxPacketSize)
if isIPv6 {
p := ipv6.NewPacketConn(conn.(*net.UDPConn))
for received < count {
n, cm, _, err := p.ReadFrom(recvBuf)
now := time.Now()
if err != nil {
break
}
if n < udpHeaderSize {
continue
}
magic := binary.BigEndian.Uint32(recvBuf[0:4])
if magic != udpMagic {
continue
}
sentNs := int64(binary.BigEndian.Uint64(recvBuf[8:16]))
totalRttNs += now.UnixNano() - sentNs
received++
if cm != nil && cm.HopLimit > 0 && !ttlFound {
ttlValue = cm.HopLimit
ttlFound = true
}
}
} else {
p := ipv4.NewPacketConn(conn.(*net.UDPConn))
for received < count {
n, cm, _, err := p.ReadFrom(recvBuf)
now := time.Now()
if err != nil {
break
}
if n < udpHeaderSize {
continue
}
magic := binary.BigEndian.Uint32(recvBuf[0:4])
if magic != udpMagic {
continue
}
sentNs := int64(binary.BigEndian.Uint64(recvBuf[8:16]))
totalRttNs += now.UnixNano() - sentNs
received++
if cm != nil && cm.TTL > 0 && !ttlFound {
ttlValue = cm.TTL
ttlFound = true
}
}
}
lossPct := float64(count-received) / float64(count) * 100.0
var hopCount int32
if ttlFound {
hopCount = estimateHops(ttlValue)
}
var avgRttS float64
if received > 0 {
avgRttS = float64(totalRttNs) / float64(received) / 1e9
}
return UDPProbeResult{
LossPct: lossPct,
HopCount: hopCount,
AvgRttS: avgRttS,
}
}
// estimateHops estimates the number of hops from the received TTL.
// Common initial TTL values are 64 (Linux) and 128 (Windows).
func estimateHops(receivedTTL int) int32 {
if receivedTTL <= 0 {
return 0
}
initialTTL := 64
if receivedTTL > 64 {
initialTTL = 128
}
hops := initialTTL - receivedTTL
if hops < 0 {
hops = 0
}
return int32(hops)
}

View File

@@ -0,0 +1,180 @@
package goldpinger
import (
"encoding/binary"
"net"
"sync/atomic"
"testing"
"time"
)
func startTestEchoListener(t *testing.T) (int, func()) {
t.Helper()
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
port := pc.LocalAddr().(*net.UDPAddr).Port
go func() {
buf := make([]byte, udpMaxPacketSize)
for {
n, addr, err := pc.ReadFrom(buf)
if err != nil {
return
}
if n >= udpHeaderSize {
magic := binary.BigEndian.Uint32(buf[0:4])
if magic == udpMagic {
pc.WriteTo(buf[:n], addr)
}
}
}
}()
return port, func() { pc.Close() }
}
// startLossyEchoListener echoes back packets but drops every dropEveryN-th
// packet (1-indexed). For example, dropEveryN=3 drops packets 3, 6, 9, etc.
func startLossyEchoListener(t *testing.T, dropEveryN int) (int, func()) {
t.Helper()
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
port := pc.LocalAddr().(*net.UDPAddr).Port
var counter atomic.Int64
go func() {
buf := make([]byte, udpMaxPacketSize)
for {
n, addr, err := pc.ReadFrom(buf)
if err != nil {
return
}
if n >= udpHeaderSize {
magic := binary.BigEndian.Uint32(buf[0:4])
if magic == udpMagic {
seq := counter.Add(1)
if seq%int64(dropEveryN) == 0 {
// Drop this packet — don't echo
continue
}
pc.WriteTo(buf[:n], addr)
}
}
}
}()
return port, func() { pc.Close() }
}
func TestProbeUDP_NoLoss(t *testing.T) {
port, cleanup := startTestEchoListener(t)
defer cleanup()
result := ProbeUDP("127.0.0.1", port, 10, 64, 2*time.Second)
if result.Err != nil {
t.Fatalf("unexpected error: %v", result.Err)
}
if result.LossPct != 0 {
t.Errorf("expected 0%% loss, got %.1f%%", result.LossPct)
}
if result.AvgRttS <= 0 {
t.Errorf("expected positive RTT, got %.6f s", result.AvgRttS)
}
t.Logf("avg UDP RTT: %.4f ms", result.AvgRttS*1000)
}
func TestProbeUDP_FullLoss(t *testing.T) {
// Bind a port then close it so nothing is listening.
// This avoids assuming a hardcoded port like 19999 is free.
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
port := pc.LocalAddr().(*net.UDPAddr).Port
pc.Close()
result := ProbeUDP("127.0.0.1", port, 5, 64, 200*time.Millisecond)
if result.LossPct != 100 {
t.Errorf("expected 100%% loss, got %.1f%%", result.LossPct)
}
}
func TestProbeUDP_PartialLoss(t *testing.T) {
tests := []struct {
name string
count int
dropEveryN int
expectedPct float64
}{
{"drop every 2nd (50%)", 10, 2, 50.0},
{"drop every 3rd (33.3%)", 9, 3, 100.0 / 3.0},
{"drop every 5th (20%)", 10, 5, 20.0},
{"drop every 10th (10%)", 10, 10, 10.0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
port, cleanup := startLossyEchoListener(t, tt.dropEveryN)
defer cleanup()
result := ProbeUDP("127.0.0.1", port, tt.count, 64, 2*time.Second)
if result.Err != nil {
t.Fatalf("unexpected error: %v", result.Err)
}
// Allow small floating point tolerance
diff := result.LossPct - tt.expectedPct
if diff < -0.1 || diff > 0.1 {
t.Errorf("expected %.1f%% loss, got %.1f%%", tt.expectedPct, result.LossPct)
}
t.Logf("loss: %.1f%% (expected %.1f%%)", result.LossPct, tt.expectedPct)
})
}
}
func TestProbeUDP_ZeroCount(t *testing.T) {
result := ProbeUDP("127.0.0.1", 12345, 0, 64, 200*time.Millisecond)
if result.Err == nil {
t.Error("expected error for count=0, got nil")
}
}
func TestProbeUDP_PacketFormat(t *testing.T) {
pkt := make([]byte, 64)
binary.BigEndian.PutUint32(pkt[0:4], udpMagic)
binary.BigEndian.PutUint32(pkt[4:8], 42)
binary.BigEndian.PutUint64(pkt[8:16], uint64(time.Now().UnixNano()))
magic := binary.BigEndian.Uint32(pkt[0:4])
if magic != 0x47504E47 {
t.Errorf("expected magic 0x47504E47, got 0x%X", magic)
}
seq := binary.BigEndian.Uint32(pkt[4:8])
if seq != 42 {
t.Errorf("expected seq 42, got %d", seq)
}
}
func TestEstimateHops(t *testing.T) {
tests := []struct {
ttl int
want int32
}{
{64, 0}, // same host, Linux
{63, 1}, // 1 hop, Linux
{56, 8}, // 8 hops, Linux
{128, 0}, // same host, Windows (TTL > 64 → initial=128)
{127, 1}, // 1 hop, Windows
{0, 0}, // invalid
}
for _, tt := range tests {
got := estimateHops(tt.ttl)
if got != tt.want {
t.Errorf("estimateHops(%d) = %d, want %d", tt.ttl, got, tt.want)
}
}
}

View File

@@ -93,10 +93,11 @@ func updatePingers(resultsChan chan<- PingAllPodsResult) {
// createPingers allocates a new pinger object for each new goldpinger Pod that's been discovered
// It also:
// (a) initializes a result object in checkResults to store info on that pod
// (b) starts a new goroutines to continuously ping the given pod.
// Each new goroutine waits for a given time before starting the continuous ping
// to prevent a thundering herd
//
// (a) initializes a result object in checkResults to store info on that pod
// (b) starts a new goroutines to continuously ping the given pod.
// Each new goroutine waits for a given time before starting the continuous ping
// to prevent a thundering herd
func createPingers(pingers map[string]*Pinger, newPods map[string]*GoldpingerPod, resultsChan chan<- PingAllPodsResult, refreshPeriod time.Duration) {
if len(newPods) == 0 {
// I have nothing to do
@@ -136,6 +137,11 @@ func destroyPingers(pingers map[string]*Pinger, deletedPods map[string]*Goldping
// Close the channel to stop pinging
close(pinger.stopChan)
// Clean up stale UDP metric labels for this peer
if GoldpingerConfig.UDPEnabled {
DeletePeerUDPMetrics(pod.HostIP, pod.PodIP)
}
// delete from pingers
delete(pingers, podName)
}

View File

@@ -2,9 +2,6 @@
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"

View File

@@ -2,11 +2,9 @@
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
stderrors "errors"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
@@ -93,11 +91,15 @@ func (m *CheckAllPodResult) validateResponse(formats strfmt.Registry) error {
if m.Response != nil {
if err := m.Response.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
ve := new(errors.Validation)
if stderrors.As(err, &ve) {
return ve.ValidateName("response")
} else if ce, ok := err.(*errors.CompositeError); ok {
}
ce := new(errors.CompositeError)
if stderrors.As(err, &ce) {
return ce.ValidateName("response")
}
return err
}
}
@@ -122,12 +124,21 @@ func (m *CheckAllPodResult) ContextValidate(ctx context.Context, formats strfmt.
func (m *CheckAllPodResult) contextValidateResponse(ctx context.Context, formats strfmt.Registry) error {
if m.Response != nil {
if swag.IsZero(m.Response) { // not required
return nil
}
if err := m.Response.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
ve := new(errors.Validation)
if stderrors.As(err, &ve) {
return ve.ValidateName("response")
} else if ce, ok := err.(*errors.CompositeError); ok {
}
ce := new(errors.CompositeError)
if stderrors.As(err, &ce) {
return ce.ValidateName("response")
}
return err
}
}

View File

@@ -2,11 +2,9 @@
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
stderrors "errors"
"strconv"
"github.com/go-openapi/errors"
@@ -73,11 +71,15 @@ func (m *CheckAllResults) validateHosts(formats strfmt.Registry) error {
if m.Hosts[i] != nil {
if err := m.Hosts[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
ve := new(errors.Validation)
if stderrors.As(err, &ve) {
return ve.ValidateName("hosts" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
}
ce := new(errors.CompositeError)
if stderrors.As(err, &ce) {
return ce.ValidateName("hosts" + "." + strconv.Itoa(i))
}
return err
}
}
@@ -117,11 +119,15 @@ func (m *CheckAllResults) validateResponses(formats strfmt.Registry) error {
}
if val, ok := m.Responses[k]; ok {
if err := val.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
ve := new(errors.Validation)
if stderrors.As(err, &ve) {
return ve.ValidateName("responses" + "." + k)
} else if ce, ok := err.(*errors.CompositeError); ok {
}
ce := new(errors.CompositeError)
if stderrors.As(err, &ce) {
return ce.ValidateName("responses" + "." + k)
}
return err
}
}
@@ -158,12 +164,21 @@ func (m *CheckAllResults) contextValidateHosts(ctx context.Context, formats strf
for i := 0; i < len(m.Hosts); i++ {
if m.Hosts[i] != nil {
if swag.IsZero(m.Hosts[i]) { // not required
return nil
}
if err := m.Hosts[i].ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
ve := new(errors.Validation)
if stderrors.As(err, &ve) {
return ve.ValidateName("hosts" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
}
ce := new(errors.CompositeError)
if stderrors.As(err, &ce) {
return ce.ValidateName("hosts" + "." + strconv.Itoa(i))
}
return err
}
}

View File

@@ -2,11 +2,9 @@
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
stderrors "errors"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
@@ -56,11 +54,15 @@ func (m *CheckResults) validatePodResults(formats strfmt.Registry) error {
}
if val, ok := m.PodResults[k]; ok {
if err := val.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
ve := new(errors.Validation)
if stderrors.As(err, &ve) {
return ve.ValidateName("podResults" + "." + k)
} else if ce, ok := err.(*errors.CompositeError); ok {
}
ce := new(errors.CompositeError)
if stderrors.As(err, &ce) {
return ce.ValidateName("podResults" + "." + k)
}
return err
}
}
@@ -77,11 +79,15 @@ func (m *CheckResults) validateProbeResults(formats strfmt.Registry) error {
if m.ProbeResults != nil {
if err := m.ProbeResults.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
ve := new(errors.Validation)
if stderrors.As(err, &ve) {
return ve.ValidateName("probeResults")
} else if ce, ok := err.(*errors.CompositeError); ok {
}
ce := new(errors.CompositeError)
if stderrors.As(err, &ce) {
return ce.ValidateName("probeResults")
}
return err
}
}
@@ -124,12 +130,20 @@ func (m *CheckResults) contextValidatePodResults(ctx context.Context, formats st
func (m *CheckResults) contextValidateProbeResults(ctx context.Context, formats strfmt.Registry) error {
if swag.IsZero(m.ProbeResults) { // not required
return nil
}
if err := m.ProbeResults.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
ve := new(errors.Validation)
if stderrors.As(err, &ve) {
return ve.ValidateName("probeResults")
} else if ce, ok := err.(*errors.CompositeError); ok {
}
ce := new(errors.CompositeError)
if stderrors.As(err, &ce) {
return ce.ValidateName("probeResults")
}
return err
}

View File

@@ -2,9 +2,6 @@
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
@@ -60,7 +57,7 @@ func (m *ClusterHealthResults) Validate(formats strfmt.Registry) error {
func (m *ClusterHealthResults) validateOK(formats strfmt.Registry) error {
if err := validate.Required("OK", "body", bool(m.OK)); err != nil {
if err := validate.Required("OK", "body", m.OK); err != nil {
return err
}

View File

@@ -2,9 +2,6 @@
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"

View File

@@ -2,11 +2,9 @@
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
stderrors "errors"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
@@ -64,11 +62,15 @@ func (m *PingResults) validateReceived(formats strfmt.Registry) error {
if m.Received != nil {
if err := m.Received.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
ve := new(errors.Validation)
if stderrors.As(err, &ve) {
return ve.ValidateName("received")
} else if ce, ok := err.(*errors.CompositeError); ok {
}
ce := new(errors.CompositeError)
if stderrors.As(err, &ce) {
return ce.ValidateName("received")
}
return err
}
}
@@ -93,12 +95,21 @@ func (m *PingResults) ContextValidate(ctx context.Context, formats strfmt.Regist
func (m *PingResults) contextValidateReceived(ctx context.Context, formats strfmt.Registry) error {
if m.Received != nil {
if swag.IsZero(m.Received) { // not required
return nil
}
if err := m.Received.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
ve := new(errors.Validation)
if stderrors.As(err, &ve) {
return ve.ValidateName("received")
} else if ce, ok := err.(*errors.CompositeError); ok {
}
ce := new(errors.CompositeError)
if stderrors.As(err, &ce) {
return ce.ValidateName("received")
}
return err
}
}

View File

@@ -2,11 +2,9 @@
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
stderrors "errors"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
@@ -37,6 +35,12 @@ type PodResult struct {
// error
Error string `json:"error,omitempty"`
// estimated network hop count from UDP TTL
HopCount int32 `json:"hop-count,omitempty"`
// UDP packet loss percentage (0-100)
LossPct float64 `json:"loss-pct,omitempty"`
// response
Response *PingResults `json:"response,omitempty"`
@@ -45,6 +49,9 @@ type PodResult struct {
// status code
StatusCode int32 `json:"status-code,omitempty"`
// average UDP round-trip time in milliseconds
UDPRttMs float64 `json:"udp-rtt-ms,omitempty"`
}
// Validate validates this pod result
@@ -116,11 +123,15 @@ func (m *PodResult) validateResponse(formats strfmt.Registry) error {
if m.Response != nil {
if err := m.Response.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
ve := new(errors.Validation)
if stderrors.As(err, &ve) {
return ve.ValidateName("response")
} else if ce, ok := err.(*errors.CompositeError); ok {
}
ce := new(errors.CompositeError)
if stderrors.As(err, &ce) {
return ce.ValidateName("response")
}
return err
}
}
@@ -145,12 +156,21 @@ func (m *PodResult) ContextValidate(ctx context.Context, formats strfmt.Registry
func (m *PodResult) contextValidateResponse(ctx context.Context, formats strfmt.Registry) error {
if m.Response != nil {
if swag.IsZero(m.Response) { // not required
return nil
}
if err := m.Response.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
ve := new(errors.Validation)
if stderrors.As(err, &ve) {
return ve.ValidateName("response")
} else if ce, ok := err.(*errors.CompositeError); ok {
}
ce := new(errors.CompositeError)
if stderrors.As(err, &ce) {
return ce.ValidateName("response")
}
return err
}
}

View File

@@ -2,9 +2,6 @@
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"

View File

@@ -2,15 +2,14 @@
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
stderrors "errors"
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
@@ -32,11 +31,15 @@ func (m ProbeResults) Validate(formats strfmt.Registry) error {
for i := 0; i < len(m[k]); i++ {
if err := m[k][i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
ve := new(errors.Validation)
if stderrors.As(err, &ve) {
return ve.ValidateName(k + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
}
ce := new(errors.CompositeError)
if stderrors.As(err, &ce) {
return ce.ValidateName(k + "." + strconv.Itoa(i))
}
return err
}
@@ -58,12 +61,20 @@ func (m ProbeResults) ContextValidate(ctx context.Context, formats strfmt.Regist
for i := 0; i < len(m[k]); i++ {
if swag.IsZero(m[k][i]) { // not required
return nil
}
if err := m[k][i].ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
ve := new(errors.Validation)
if stderrors.As(err, &ve) {
return ve.ValidateName(k + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
}
ce := new(errors.CompositeError)
if stderrors.As(err, &ce) {
return ce.ValidateName(k + "." + strconv.Itoa(i))
}
return err
}

View File

@@ -2,17 +2,17 @@
// Package restapi Goldpinger
//
// Schemes:
// http
// Host: localhost
// BasePath: /
// Version: 3.0.0
// Schemes:
// http
// Host: localhost
// BasePath: /
// Version: 3.0.0
//
// Consumes:
// - application/json
// Consumes:
// - application/json
//
// Produces:
// - application/json
// Produces:
// - application/json
//
// swagger:meta
package restapi

View File

@@ -2,9 +2,6 @@
package restapi
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"encoding/json"
)
@@ -170,12 +167,6 @@ func init() {
"type": "boolean",
"default": false
},
"dnsResults": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/DnsResults"
}
},
"hosts": {
"type": "array",
"items": {
@@ -203,10 +194,10 @@ func init() {
"type": "integer",
"format": "int32"
},
"httpResults": {
"probeResults": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/HttpResults"
"$ref": "#/definitions/ProbeResults"
}
},
"responses": {
@@ -214,32 +205,20 @@ func init() {
"additionalProperties": {
"$ref": "#/definitions/CheckAllPodResult"
}
},
"tcpResults": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/TcpResults"
}
}
}
},
"CheckResults": {
"type": "object",
"properties": {
"dnsResults": {
"$ref": "#/definitions/DnsResults"
},
"httpResults": {
"$ref": "#/definitions/HttpResults"
},
"podResults": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/PodResult"
}
},
"tcpResults": {
"$ref": "#/definitions/TcpResults"
"probeResults": {
"$ref": "#/definitions/ProbeResults"
}
}
},
@@ -279,12 +258,6 @@ func init() {
}
}
},
"DnsResults": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/ProbeResult"
}
},
"HealthCheckResults": {
"type": "object",
"properties": {
@@ -302,12 +275,6 @@ func init() {
}
}
},
"HttpResults": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/ProbeResult"
}
},
"PingResults": {
"type": "object",
"properties": {
@@ -342,6 +309,16 @@ func init() {
"error": {
"type": "string"
},
"hop-count": {
"description": "estimated network hop count from UDP TTL",
"type": "integer",
"format": "int32"
},
"loss-pct": {
"description": "UDP packet loss percentage (0-100)",
"type": "number",
"format": "double"
},
"response": {
"$ref": "#/definitions/PingResults"
},
@@ -353,6 +330,11 @@ func init() {
"status-code": {
"type": "integer",
"format": "int32"
},
"udp-rtt-ms": {
"description": "average UDP round-trip time in milliseconds",
"type": "number",
"format": "double"
}
}
},
@@ -361,16 +343,22 @@ func init() {
"error": {
"type": "string"
},
"protocol": {
"type": "string"
},
"response-time-ms": {
"type": "number",
"format": "int64"
}
}
},
"TcpResults": {
"ProbeResults": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/ProbeResult"
"type": "array",
"items": {
"$ref": "#/definitions/ProbeResult"
}
}
}
}
@@ -528,12 +516,6 @@ func init() {
"type": "boolean",
"default": false
},
"dnsResults": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/DnsResults"
}
},
"hosts": {
"type": "array",
"items": {
@@ -548,10 +530,10 @@ func init() {
"type": "integer",
"format": "int32"
},
"httpResults": {
"probeResults": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/HttpResults"
"$ref": "#/definitions/ProbeResults"
}
},
"responses": {
@@ -559,12 +541,6 @@ func init() {
"additionalProperties": {
"$ref": "#/definitions/CheckAllPodResult"
}
},
"tcpResults": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/TcpResults"
}
}
}
},
@@ -587,20 +563,14 @@ func init() {
"CheckResults": {
"type": "object",
"properties": {
"dnsResults": {
"$ref": "#/definitions/DnsResults"
},
"httpResults": {
"$ref": "#/definitions/HttpResults"
},
"podResults": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/PodResult"
}
},
"tcpResults": {
"$ref": "#/definitions/TcpResults"
"probeResults": {
"$ref": "#/definitions/ProbeResults"
}
}
},
@@ -640,12 +610,6 @@ func init() {
}
}
},
"DnsResults": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/ProbeResult"
}
},
"HealthCheckResults": {
"type": "object",
"properties": {
@@ -663,12 +627,6 @@ func init() {
}
}
},
"HttpResults": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/ProbeResult"
}
},
"PingResults": {
"type": "object",
"properties": {
@@ -703,6 +661,16 @@ func init() {
"error": {
"type": "string"
},
"hop-count": {
"description": "estimated network hop count from UDP TTL",
"type": "integer",
"format": "int32"
},
"loss-pct": {
"description": "UDP packet loss percentage (0-100)",
"type": "number",
"format": "double"
},
"response": {
"$ref": "#/definitions/PingResults"
},
@@ -714,6 +682,11 @@ func init() {
"status-code": {
"type": "integer",
"format": "int32"
},
"udp-rtt-ms": {
"description": "average UDP round-trip time in milliseconds",
"type": "number",
"format": "double"
}
}
},
@@ -722,16 +695,22 @@ func init() {
"error": {
"type": "string"
},
"protocol": {
"type": "string"
},
"response-time-ms": {
"type": "number",
"format": "int64"
}
}
},
"TcpResults": {
"ProbeResults": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/ProbeResult"
"type": "array",
"items": {
"$ref": "#/definitions/ProbeResult"
}
}
}
}

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"net/http"
@@ -29,10 +26,10 @@ func NewCheckAllPods(ctx *middleware.Context, handler CheckAllPodsHandler) *Chec
return &CheckAllPods{Context: ctx, Handler: handler}
}
/* CheckAllPods swagger:route GET /check_all checkAllPods
/*
CheckAllPods swagger:route GET /check_all checkAllPods
Queries the API server for all other pods in this service, and makes all of them query all of their neighbours, using their pods IPs. Calls their /check endpoint.
*/
type CheckAllPods struct {
Context *middleware.Context
@@ -51,6 +48,7 @@ func (o *CheckAllPods) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
}
res := o.Handler.Handle(Params) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
@@ -25,7 +22,6 @@ func NewCheckAllPodsParams() CheckAllPodsParams {
//
// swagger:parameters checkAllPods
type CheckAllPodsParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
}

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
@@ -16,7 +13,8 @@ import (
// CheckAllPodsOKCode is the HTTP code returned for type CheckAllPodsOK
const CheckAllPodsOKCode int = 200
/*CheckAllPodsOK Success, return response
/*
CheckAllPodsOK Success, return response
swagger:response checkAllPodsOK
*/

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"errors"
"net/url"

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"net/http"
@@ -29,10 +26,10 @@ func NewCheckServicePods(ctx *middleware.Context, handler CheckServicePodsHandle
return &CheckServicePods{Context: ctx, Handler: handler}
}
/* CheckServicePods swagger:route GET /check checkServicePods
/*
CheckServicePods swagger:route GET /check checkServicePods
Queries the API server for all other pods in this service, and pings them via their pods IPs. Calls their /ping endpoint
*/
type CheckServicePods struct {
Context *middleware.Context
@@ -51,6 +48,7 @@ func (o *CheckServicePods) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
}
res := o.Handler.Handle(Params) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
@@ -25,7 +22,6 @@ func NewCheckServicePodsParams() CheckServicePodsParams {
//
// swagger:parameters checkServicePods
type CheckServicePodsParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
}

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
@@ -16,7 +13,8 @@ import (
// CheckServicePodsOKCode is the HTTP code returned for type CheckServicePodsOK
const CheckServicePodsOKCode int = 200
/*CheckServicePodsOK Success, return response
/*
CheckServicePodsOK Success, return response
swagger:response checkServicePodsOK
*/

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"errors"
"net/url"

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"net/http"
@@ -29,10 +26,10 @@ func NewClusterHealth(ctx *middleware.Context, handler ClusterHealthHandler) *Cl
return &ClusterHealth{Context: ctx, Handler: handler}
}
/* ClusterHealth swagger:route GET /cluster_health clusterHealth
/*
ClusterHealth swagger:route GET /cluster_health clusterHealth
Checks the full graph. Returns a binary OK or not OK.
*/
type ClusterHealth struct {
Context *middleware.Context
@@ -51,6 +48,7 @@ func (o *ClusterHealth) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
}
res := o.Handler.Handle(Params) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
@@ -25,7 +22,6 @@ func NewClusterHealthParams() ClusterHealthParams {
//
// swagger:parameters clusterHealth
type ClusterHealthParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
}

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
@@ -16,7 +13,8 @@ import (
// ClusterHealthOKCode is the HTTP code returned for type ClusterHealthOK
const ClusterHealthOKCode int = 200
/*ClusterHealthOK Healthy cluster
/*
ClusterHealthOK Healthy cluster
swagger:response clusterHealthOK
*/
@@ -60,7 +58,8 @@ func (o *ClusterHealthOK) WriteResponse(rw http.ResponseWriter, producer runtime
// ClusterHealthIMATeapotCode is the HTTP code returned for type ClusterHealthIMATeapot
const ClusterHealthIMATeapotCode int = 418
/*ClusterHealthIMATeapot Unhealthy cluster
/*
ClusterHealthIMATeapot Unhealthy cluster
swagger:response clusterHealthIMATeapot
*/

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"errors"
"net/url"

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"fmt"
"net/http"
@@ -43,18 +40,32 @@ func NewGoldpingerAPI(spec *loads.Document) *GoldpingerAPI {
JSONProducer: runtime.JSONProducer(),
CheckAllPodsHandler: CheckAllPodsHandlerFunc(func(params CheckAllPodsParams) middleware.Responder {
_ = params
return middleware.NotImplemented("operation CheckAllPods has not yet been implemented")
}),
CheckServicePodsHandler: CheckServicePodsHandlerFunc(func(params CheckServicePodsParams) middleware.Responder {
_ = params
return middleware.NotImplemented("operation CheckServicePods has not yet been implemented")
}),
ClusterHealthHandler: ClusterHealthHandlerFunc(func(params ClusterHealthParams) middleware.Responder {
_ = params
return middleware.NotImplemented("operation ClusterHealth has not yet been implemented")
}),
HealthzHandler: HealthzHandlerFunc(func(params HealthzParams) middleware.Responder {
_ = params
return middleware.NotImplemented("operation Healthz has not yet been implemented")
}),
PingHandler: PingHandlerFunc(func(params PingParams) middleware.Responder {
_ = params
return middleware.NotImplemented("operation Ping has not yet been implemented")
}),
}
@@ -120,7 +131,7 @@ type GoldpingerAPI struct {
CommandLineOptionsGroups []swag.CommandLineOptionsGroup
// User defined logger function.
Logger func(string, ...interface{})
Logger func(string, ...any)
}
// UseRedoc for documentation at /docs
@@ -219,12 +230,12 @@ func (o *GoldpingerAPI) Authorizer() runtime.Authorizer {
}
// ConsumersFor gets the consumers for the specified media types.
//
// MIME type parameters are ignored here.
func (o *GoldpingerAPI) ConsumersFor(mediaTypes []string) map[string]runtime.Consumer {
result := make(map[string]runtime.Consumer, len(mediaTypes))
for _, mt := range mediaTypes {
switch mt {
case "application/json":
if mt == "application/json" {
result["application/json"] = o.JSONConsumer
}
@@ -232,16 +243,17 @@ func (o *GoldpingerAPI) ConsumersFor(mediaTypes []string) map[string]runtime.Con
result[mt] = c
}
}
return result
}
// ProducersFor gets the producers for the specified media types.
//
// MIME type parameters are ignored here.
func (o *GoldpingerAPI) ProducersFor(mediaTypes []string) map[string]runtime.Producer {
result := make(map[string]runtime.Producer, len(mediaTypes))
for _, mt := range mediaTypes {
switch mt {
case "application/json":
if mt == "application/json" {
result["application/json"] = o.JSONProducer
}
@@ -249,6 +261,7 @@ func (o *GoldpingerAPI) ProducersFor(mediaTypes []string) map[string]runtime.Pro
result[mt] = p
}
}
return result
}
@@ -344,6 +357,6 @@ func (o *GoldpingerAPI) AddMiddlewareFor(method, path string, builder middleware
}
o.Init()
if h, ok := o.handlers[um][path]; ok {
o.handlers[method][path] = builder(h)
o.handlers[um][path] = builder(h)
}
}

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"net/http"
@@ -29,10 +26,10 @@ func NewHealthz(ctx *middleware.Context, handler HealthzHandler) *Healthz {
return &Healthz{Context: ctx, Handler: handler}
}
/* Healthz swagger:route GET /healthz healthz
/*
Healthz swagger:route GET /healthz healthz
The healthcheck endpoint provides detailed information about the health of a web service. If each of the components required by the service are healthy, then the service is considered healthy and will return a 200 OK response. If any of the components needed by the service are unhealthy, then a 503 Service Unavailable response will be provided.
*/
type Healthz struct {
Context *middleware.Context
@@ -51,6 +48,7 @@ func (o *Healthz) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
}
res := o.Handler.Handle(Params) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
@@ -25,7 +22,6 @@ func NewHealthzParams() HealthzParams {
//
// swagger:parameters healthz
type HealthzParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
}

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
@@ -16,7 +13,8 @@ import (
// HealthzOKCode is the HTTP code returned for type HealthzOK
const HealthzOKCode int = 200
/*HealthzOK Health check report
/*
HealthzOK Health check report
swagger:response healthzOK
*/
@@ -60,7 +58,8 @@ func (o *HealthzOK) WriteResponse(rw http.ResponseWriter, producer runtime.Produ
// HealthzServiceUnavailableCode is the HTTP code returned for type HealthzServiceUnavailable
const HealthzServiceUnavailableCode int = 503
/*HealthzServiceUnavailable Unhealthy service
/*
HealthzServiceUnavailable Unhealthy service
swagger:response healthzServiceUnavailable
*/

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"errors"
"net/url"

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"net/http"
@@ -29,10 +26,10 @@ func NewPing(ctx *middleware.Context, handler PingHandler) *Ping {
return &Ping{Context: ctx, Handler: handler}
}
/* Ping swagger:route GET /ping ping
/*
Ping swagger:route GET /ping ping
return query stats
*/
type Ping struct {
Context *middleware.Context
@@ -51,6 +48,7 @@ func (o *Ping) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
}
res := o.Handler.Handle(Params) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
@@ -25,7 +22,6 @@ func NewPingParams() PingParams {
//
// swagger:parameters ping
type PingParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
}

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
@@ -16,7 +13,8 @@ import (
// PingOKCode is the HTTP code returned for type PingOK
const PingOKCode int = 200
/*PingOK return success
/*
PingOK return success
swagger:response pingOK
*/

View File

@@ -2,9 +2,6 @@
package operations
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"errors"
"net/url"

View File

@@ -7,8 +7,6 @@ import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
@@ -20,11 +18,12 @@ import (
"syscall"
"time"
"github.com/go-openapi/runtime/flagext"
"github.com/go-openapi/swag"
flags "github.com/jessevdk/go-flags"
"golang.org/x/net/netutil"
"github.com/go-openapi/runtime/flagext"
"github.com/go-openapi/swag"
"github.com/bloomberg/goldpinger/v3/pkg/restapi/operations"
)
@@ -81,7 +80,7 @@ type Server struct {
ListenLimit int `long:"listen-limit" description:"limit the number of outstanding requests"`
KeepAlive time.Duration `long:"keep-alive" description:"sets the TCP keep-alive timeouts on accepted connections. It prunes dead TCP connections ( e.g. closing laptop mid-download)" default:"3m"`
ReadTimeout time.Duration `long:"read-timeout" description:"maximum duration before timing out read of the request" default:"30s"`
WriteTimeout time.Duration `long:"write-timeout" description:"maximum duration before timing out write of the response" default:"60s"`
WriteTimeout time.Duration `long:"write-timeout" description:"maximum duration before timing out write of the response" default:"30s"`
httpServerL net.Listener
TLSHost string `long:"tls-host" description:"the IP to listen on for tls, when not specified it's the same as --host" env:"TLS_HOST"`
@@ -105,7 +104,7 @@ type Server struct {
}
// Logf logs message either via defined user logger or via system one if no user logger is defined.
func (s *Server) Logf(f string, args ...interface{}) {
func (s *Server) Logf(f string, args ...any) {
if s.api != nil && s.api.Logger != nil {
s.api.Logger(f, args...)
} else {
@@ -115,7 +114,7 @@ func (s *Server) Logf(f string, args ...interface{}) {
// Fatalf logs message either via defined user logger or via system one if no user logger is defined.
// Exits with non-zero status after printing
func (s *Server) Fatalf(f string, args ...interface{}) {
func (s *Server) Fatalf(f string, args ...any) {
if s.api != nil && s.api.Logger != nil {
s.api.Logger(f, args...)
os.Exit(1)
@@ -189,8 +188,8 @@ func (s *Server) Serve() (err error) {
s.Logf("Serving goldpinger at unix://%s", s.SocketPath)
go func(l net.Listener) {
defer wg.Done()
if err := domainSocket.Serve(l); err != nil && err != http.ErrServerClosed {
s.Fatalf("%v", err)
if errServe := domainSocket.Serve(l); errServe != nil && !errors.Is(errServe, http.ErrServerClosed) {
s.Fatalf("%v", errServe)
}
s.Logf("Stopped serving goldpinger at unix://%s", s.SocketPath)
}(s.domainSocketL)
@@ -219,8 +218,8 @@ func (s *Server) Serve() (err error) {
s.Logf("Serving goldpinger at http://%s", s.httpServerL.Addr())
go func(l net.Listener) {
defer wg.Done()
if err := httpServer.Serve(l); err != nil && err != http.ErrServerClosed {
s.Fatalf("%v", err)
if errServe := httpServer.Serve(l); errServe != nil && !errors.Is(errServe, http.ErrServerClosed) {
s.Fatalf("%v", errServe)
}
s.Logf("Stopped serving goldpinger at http://%s", l.Addr())
}(s.httpServerL)
@@ -274,14 +273,14 @@ func (s *Server) Serve() (err error) {
if s.TLSCACertificate != "" {
// include specified CA certificate
caCert, caCertErr := ioutil.ReadFile(string(s.TLSCACertificate))
caCert, caCertErr := os.ReadFile(string(s.TLSCACertificate))
if caCertErr != nil {
return caCertErr
}
caCertPool := x509.NewCertPool()
ok := caCertPool.AppendCertsFromPEM(caCert)
if !ok {
return fmt.Errorf("cannot parse CA certificate")
return errors.New("cannot parse CA certificate")
}
httpsServer.TLSConfig.ClientCAs = caCertPool
httpsServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
@@ -312,8 +311,8 @@ func (s *Server) Serve() (err error) {
s.Logf("Serving goldpinger at https://%s", s.httpsServerL.Addr())
go func(l net.Listener) {
defer wg.Done()
if err := httpsServer.Serve(l); err != nil && err != http.ErrServerClosed {
s.Fatalf("%v", err)
if errServe := httpsServer.Serve(l); errServe != nil && !errors.Is(errServe, http.ErrServerClosed) {
s.Fatalf("%v", errServe)
}
s.Logf("Stopped serving goldpinger at https://%s", l.Addr())
}(tls.NewListener(s.httpsServerL, httpsServer.TLSConfig))

View File

@@ -378,16 +378,33 @@ var main = function(timeout){
if (!edge._data.OK) {
color = "red";
}
if (edge._data.OK && edge._data["loss-pct"] > 0) {
color = "#f0ad4e";
}
if ("isprobeResultsNode" in edge._data) {
type = "dashed";
}
var label = "";
if (edge._data["udp-rtt-ms"] > 0) {
label = edge._data["udp-rtt-ms"].toFixed(2) + "ms udp";
} else if (edge._data["response-time-ms"]) {
label = edge._data["response-time-ms"] + "ms";
} else if (edge._data.elapsed) {
label = edge._data.elapsed;
}
if (edge._data["loss-pct"] > 0) {
label += " " + edge._data["loss-pct"].toFixed(1) + "% loss";
}
if (edge._data["hop-count"] > 0) {
label += " " + edge._data["hop-count"] + " hops";
}
var edge = {
id: "e" + i,
source: edge.source,
target: edge.target,
type: type,
color: color,
label: edge._data.elapsed,
label: label,
size: 7
}
//console.log(edge);

View File

@@ -61,6 +61,18 @@ definitions:
type: number
format: int64
description: wall clock time in milliseconds
loss-pct:
type: number
format: double
description: UDP packet loss percentage (0-100)
hop-count:
type: integer
format: int32
description: estimated network hop count from UDP TTL
udp-rtt-ms:
type: number
format: double
description: average UDP round-trip time in milliseconds
CheckResults:
type: object
properties: