ipv6 network test
This commit is contained in:
@@ -52,22 +52,16 @@ func MainApp(configPath string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
repo := http.NewIPRepository(log.Named("network_test"))
|
repoClient4 := http.NewHTTPClient("", 0, 1, false, config, log.Named("network_test"))
|
||||||
repoClient := http.NewHTTPClient(
|
repoClient6 := http.NewHTTPClient("", 0, 1, true, config, log.Named("network_test"))
|
||||||
"",
|
repo := http.NewIPRepository(repoClient4, repoClient6, log.Named("network_test"))
|
||||||
0,
|
repo.NetworkTest(true)
|
||||||
1,
|
|
||||||
true,
|
|
||||||
config,
|
|
||||||
log.Named("network_test"),
|
|
||||||
)
|
|
||||||
repo.NetworkTest(repoClient, false)
|
|
||||||
|
|
||||||
apiClient := http.NewHTTPClient(
|
apiClient := http.NewHTTPClient(
|
||||||
config.GetToken(),
|
config.GetToken(),
|
||||||
config.GetRetriesUntilFailed(), // default retries = 2
|
config.GetRetriesUntilFailed(), // default retries = 2
|
||||||
config.GetApiTimeoutSecs(), // default api timeout = 60
|
config.GetApiTimeoutSecs(), // default api timeout = 60
|
||||||
false, // ipv6 support is not needed for api client
|
false, // no need for ipv6 support
|
||||||
config,
|
config,
|
||||||
log.Named("api_client"),
|
log.Named("api_client"),
|
||||||
)
|
)
|
||||||
@@ -76,16 +70,16 @@ func MainApp(configPath string) {
|
|||||||
config.GetToken(),
|
config.GetToken(),
|
||||||
config.GetRetriesUntilFailed(), // default retries = 2
|
config.GetRetriesUntilFailed(), // default retries = 2
|
||||||
config.GetDownloadTimeoutSecs(), // default download timeout = 10
|
config.GetDownloadTimeoutSecs(), // default download timeout = 10
|
||||||
false, // this is also api client, so no ipv6 support
|
false, // no need for ipv6 support
|
||||||
config,
|
config,
|
||||||
log.Named("unrestrict_client"),
|
log.Named("unrestrict_client"),
|
||||||
)
|
)
|
||||||
|
|
||||||
downloadClient := http.NewHTTPClient(
|
downloadClient := http.NewHTTPClient(
|
||||||
"", // no token required for download client
|
"",
|
||||||
config.GetRetriesUntilFailed(), //
|
config.GetRetriesUntilFailed(),
|
||||||
config.GetDownloadTimeoutSecs(), //
|
config.GetDownloadTimeoutSecs(),
|
||||||
true, // set as download client
|
config.ShouldForceIPv6(),
|
||||||
config,
|
config,
|
||||||
log.Named("download_client"),
|
log.Named("download_client"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/debridmediamanager/zurg/internal/config"
|
"github.com/debridmediamanager/zurg/internal/config"
|
||||||
"github.com/debridmediamanager/zurg/pkg/hosts"
|
|
||||||
"github.com/debridmediamanager/zurg/pkg/logutil"
|
"github.com/debridmediamanager/zurg/pkg/logutil"
|
||||||
http_dialer "github.com/mwitkow/go-http-dialer"
|
http_dialer "github.com/mwitkow/go-http-dialer"
|
||||||
"golang.org/x/net/proxy"
|
"golang.org/x/net/proxy"
|
||||||
@@ -29,9 +28,8 @@ type HTTPClient struct {
|
|||||||
timeoutSecs int
|
timeoutSecs int
|
||||||
backoff func(attempt int) time.Duration
|
backoff func(attempt int) time.Duration
|
||||||
bearerToken string
|
bearerToken string
|
||||||
isDownloadClient bool
|
|
||||||
cfg config.ConfigInterface
|
cfg config.ConfigInterface
|
||||||
ipv6 cmap.ConcurrentMap[string, string]
|
dnsCache cmap.ConcurrentMap[string, string]
|
||||||
ipv6Hosts []string
|
ipv6Hosts []string
|
||||||
log *logutil.Logger
|
log *logutil.Logger
|
||||||
}
|
}
|
||||||
@@ -49,7 +47,7 @@ func NewHTTPClient(
|
|||||||
token string,
|
token string,
|
||||||
maxRetries int,
|
maxRetries int,
|
||||||
timeoutSecs int,
|
timeoutSecs int,
|
||||||
isDownloadClient bool,
|
forceIPv6 bool,
|
||||||
cfg config.ConfigInterface,
|
cfg config.ConfigInterface,
|
||||||
log *logutil.Logger,
|
log *logutil.Logger,
|
||||||
) *HTTPClient {
|
) *HTTPClient {
|
||||||
@@ -59,9 +57,9 @@ func NewHTTPClient(
|
|||||||
maxRetries: maxRetries,
|
maxRetries: maxRetries,
|
||||||
timeoutSecs: timeoutSecs,
|
timeoutSecs: timeoutSecs,
|
||||||
backoff: backoffFunc,
|
backoff: backoffFunc,
|
||||||
isDownloadClient: isDownloadClient,
|
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
ipv6: cmap.New[string](),
|
dnsCache: cmap.New[string](),
|
||||||
|
ipv6Hosts: []string{},
|
||||||
log: log,
|
log: log,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,21 +94,11 @@ func NewHTTPClient(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.ShouldForceIPv6() {
|
if forceIPv6 {
|
||||||
// fetch IPv6 hosts
|
|
||||||
ipv6List, err := hosts.FetchHosts(hosts.IPV6)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Failed to fetch IPv6 hosts: %v", err)
|
|
||||||
// Decide if you want to return nil here or continue without IPv6
|
|
||||||
} else {
|
|
||||||
client.ipv6Hosts = ipv6List
|
|
||||||
log.Debugf("Fetched %d IPv6 hosts", len(ipv6List))
|
|
||||||
}
|
|
||||||
|
|
||||||
// replace the default dialer with a custom one that resolves hostnames to IPv6 addresses
|
// replace the default dialer with a custom one that resolves hostnames to IPv6 addresses
|
||||||
client.client.Transport.(*http.Transport).DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
|
client.client.Transport.(*http.Transport).DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
// if address is already cached, use it
|
// if address is already cached, use it
|
||||||
if ipv6Address, ok := client.ipv6.Get(address); ok {
|
if ipv6Address, ok := client.dnsCache.Get(address); ok {
|
||||||
return dialer.Dial(network, ipv6Address)
|
return dialer.Dial(network, ipv6Address)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,26 +114,15 @@ func NewHTTPClient(
|
|||||||
for _, ip := range ips {
|
for _, ip := range ips {
|
||||||
if ip.IP.To4() == nil { // IPv6 address found
|
if ip.IP.To4() == nil { // IPv6 address found
|
||||||
ipv6Address := net.JoinHostPort(ip.IP.String(), port)
|
ipv6Address := net.JoinHostPort(ip.IP.String(), port)
|
||||||
client.ipv6.Set(address, ipv6Address)
|
client.dnsCache.Set(address, ipv6Address)
|
||||||
return dialer.Dial(network, ipv6Address)
|
return dialer.Dial(network, ipv6Address)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// no IPv6 address found, use the original address
|
return nil, fmt.Errorf("no ipv6 address found")
|
||||||
log.Warnf("No IPv6 address found for host %s", host)
|
|
||||||
for _, ip := range ips {
|
|
||||||
if ip.IP.To4() != nil { // IPv4 address found
|
|
||||||
ipV4Address := net.JoinHostPort(ip.IP.String(), port)
|
|
||||||
client.ipv6.Set(address, ipV4Address)
|
|
||||||
return dialer.Dial(network, ipV4Address)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dialer.Dial(network, address)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return &client
|
return &client
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +148,7 @@ func (r *HTTPClient) Do(req *http.Request) (*http.Response, error) {
|
|||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
r.replaceWithIPv6Host(req) // needed for ipv6
|
// r.optimizeHost(req)
|
||||||
|
|
||||||
resp, err = r.client.Do(req)
|
resp, err = r.client.Do(req)
|
||||||
|
|
||||||
@@ -210,12 +187,7 @@ func (r *HTTPClient) Do(req *http.Request) (*http.Response, error) {
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *HTTPClient) replaceWithIPv6Host(req *http.Request) {
|
func (r *HTTPClient) optimizeHost(req *http.Request) {
|
||||||
// don't replace host if IPv6 is not supported or not forced
|
|
||||||
if !r.isDownloadClient || !r.cfg.ShouldForceIPv6() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// this host should be replaced
|
|
||||||
if !strings.HasSuffix(req.Host, ".download.real-debrid.com") {
|
if !strings.HasSuffix(req.Host, ".download.real-debrid.com") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -233,7 +205,6 @@ func (r *HTTPClient) replaceWithIPv6Host(req *http.Request) {
|
|||||||
req.Host = r.ipv6Hosts[rand.Intn(len(r.ipv6Hosts))]
|
req.Host = r.ipv6Hosts[rand.Intn(len(r.ipv6Hosts))]
|
||||||
req.URL.Host = req.Host
|
req.URL.Host = req.Host
|
||||||
r.log.Debugf("Host %s is not a valid IPv6 host, assigning a random IPv6 host: %s", req.URL.Host, req.Host)
|
r.log.Debugf("Host %s is not a valid IPv6 host, assigning a random IPv6 host: %s", req.URL.Host, req.Host)
|
||||||
// if !found && !r.CanFetchFirstByte(req.URL.String()) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *HTTPClient) proxyDialer(proxyURL *url.URL) (proxy.Dialer, error) {
|
func (r *HTTPClient) proxyDialer(proxyURL *url.URL) (proxy.Dialer, error) {
|
||||||
|
|||||||
158
pkg/http/ip.go
158
pkg/http/ip.go
@@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
@@ -15,113 +14,126 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type IPRepository struct {
|
type IPRepository struct {
|
||||||
ipv4 []string
|
ipv4client *HTTPClient
|
||||||
ipv6 []string
|
ipv6client *HTTPClient
|
||||||
latencyMap map[string]float64
|
ipv4latencyMap map[string]float64
|
||||||
|
ipv6latencyMap map[string]float64
|
||||||
log *logutil.Logger
|
log *logutil.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIPRepository(log *logutil.Logger) *IPRepository {
|
func NewIPRepository(ipv4client *HTTPClient, ipv6client *HTTPClient, log *logutil.Logger) *IPRepository {
|
||||||
repo := &IPRepository{
|
repo := &IPRepository{
|
||||||
ipv4: []string{},
|
ipv4client: ipv4client,
|
||||||
ipv6: []string{},
|
ipv6client: ipv6client,
|
||||||
latencyMap: make(map[string]float64),
|
ipv4latencyMap: make(map[string]float64),
|
||||||
|
ipv6latencyMap: make(map[string]float64),
|
||||||
log: log,
|
log: log,
|
||||||
}
|
}
|
||||||
|
|
||||||
repo.lookupDomains()
|
|
||||||
|
|
||||||
return repo
|
return repo
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *IPRepository) NetworkTest(downloadClient *HTTPClient, forceRun bool) {
|
func (r *IPRepository) NetworkTest(forceRun bool) {
|
||||||
latencyFile := "data/latency.json"
|
ipv4latencyFile := "data/latency4.json"
|
||||||
|
ipv6latencyFile := "data/latency6.json"
|
||||||
if !forceRun {
|
if !forceRun {
|
||||||
latencyData := r.readLatencyFile(latencyFile)
|
latencyData := r.readLatencyFile(ipv4latencyFile)
|
||||||
if latencyData != nil {
|
if latencyData != nil {
|
||||||
r.latencyMap = *latencyData
|
r.ipv4latencyMap = *latencyData
|
||||||
return
|
}
|
||||||
|
latencyData = r.readLatencyFile(ipv6latencyFile)
|
||||||
|
if latencyData != nil {
|
||||||
|
r.ipv6latencyMap = *latencyData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r.log.Info("Network test will start now. Note that it will only run once and record the latency of each domain for future use.")
|
r.log.Info("Network test will start now. IGNORE THE WARNINGS!")
|
||||||
r.latencyTest(downloadClient)
|
r.runLatencyTest()
|
||||||
r.log.Infof("Network test completed. Saving the results to %s", latencyFile)
|
r.log.Infof("Network test completed. Saving the results to %s and %s", ipv4latencyFile, ipv6latencyFile)
|
||||||
r.writeLatencyFile(latencyFile)
|
r.log.Debugf("ipv4 %v", r.ipv4latencyMap)
|
||||||
|
r.log.Debugf("ipv6 %v", r.ipv6latencyMap)
|
||||||
|
r.writeLatencyFile(ipv4latencyFile, r.ipv4latencyMap)
|
||||||
|
r.writeLatencyFile(ipv6latencyFile, r.ipv6latencyMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *IPRepository) lookupDomains() {
|
func (r *IPRepository) runLatencyTest() {
|
||||||
limit := 99
|
limit := 99
|
||||||
increment := 10
|
|
||||||
start := 0
|
start := 0
|
||||||
for {
|
for {
|
||||||
lastDomainWorked := false
|
lastDomainsWorked := false
|
||||||
for i := start; i <= limit; i++ {
|
for i := start; i <= limit; i++ {
|
||||||
domain := fmt.Sprintf("%d.download.real-debrid.com", i)
|
domain := fmt.Sprintf("%d.download.real-debrid.com", i)
|
||||||
ips, err := net.LookupIP(domain)
|
// ips, err := net.LookupIP(domain)
|
||||||
if err == nil && len(ips) > 0 {
|
// if err != nil || len(ips) == 0 {
|
||||||
hasIPv6 := false
|
// continue
|
||||||
for _, ip := range ips {
|
// }
|
||||||
if ip.To4() == nil {
|
|
||||||
hasIPv6 = true
|
latency, err := r.testDomainLatency(r.ipv4client, domain)
|
||||||
}
|
if err == nil {
|
||||||
}
|
r.ipv4latencyMap[domain] = latency
|
||||||
// assume it always has ipv4
|
r.log.Debugf("Latency from ipv4 %s: %.5f seconds", domain, latency)
|
||||||
r.ipv4 = append(r.ipv4, domain)
|
if i >= limit-2 {
|
||||||
if hasIPv6 {
|
lastDomainsWorked = true
|
||||||
r.ipv6 = append(r.ipv6, domain)
|
|
||||||
}
|
|
||||||
if i == limit {
|
|
||||||
lastDomainWorked = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
domain2 := fmt.Sprintf("%d.download.real-debrid.cloud", i)
|
latency, err = r.testDomainLatency(r.ipv6client, domain)
|
||||||
ips2, err := net.LookupIP(domain2)
|
if err == nil {
|
||||||
if err == nil && len(ips2) > 0 {
|
r.ipv6latencyMap[domain] = latency
|
||||||
hasIPv6 := false
|
r.log.Debugf("Latency from ipv6 %s: %.5f seconds", domain, latency)
|
||||||
for _, ip := range ips {
|
if i >= limit-2 {
|
||||||
if ip.To4() == nil {
|
lastDomainsWorked = true
|
||||||
hasIPv6 = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r.ipv4 = append(r.ipv4, domain2)
|
|
||||||
if hasIPv6 {
|
domain = fmt.Sprintf("%d.download.real-debrid.cloud", i)
|
||||||
r.ipv6 = append(r.ipv6, domain2)
|
// ips, err = net.LookupIP(domain)
|
||||||
|
// if err != nil || len(ips) == 0 {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
latency, err = r.testDomainLatency(r.ipv4client, domain)
|
||||||
|
if err == nil {
|
||||||
|
r.ipv4latencyMap[domain] = latency
|
||||||
|
r.log.Debugf("Latency from ipv4 %s: %.5f seconds", domain, latency)
|
||||||
|
if i >= limit-2 {
|
||||||
|
lastDomainsWorked = true
|
||||||
}
|
}
|
||||||
if i == limit {
|
}
|
||||||
lastDomainWorked = true
|
|
||||||
|
latency, err = r.testDomainLatency(r.ipv6client, domain)
|
||||||
|
if err == nil {
|
||||||
|
r.ipv6latencyMap[domain] = latency
|
||||||
|
r.log.Debugf("Latency from ipv6 %s: %.5f seconds", domain, latency)
|
||||||
|
if i >= limit-2 {
|
||||||
|
lastDomainsWorked = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if lastDomainWorked {
|
if lastDomainsWorked {
|
||||||
start = limit + 1
|
start = limit + 1
|
||||||
limit += increment
|
limit += 10
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r.log.Infof("Found %d IPv4 domains and %d IPv6 domains", len(r.ipv4), len(r.ipv6))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *IPRepository) latencyTest(downloadClient *HTTPClient) {
|
func (r *IPRepository) testDomainLatency(client *HTTPClient, domain string) (float64, error) {
|
||||||
const testFileSize = 1
|
const testFileSize = 1 // byte
|
||||||
const iterations = 3
|
const iterations = 3
|
||||||
|
|
||||||
for _, domain := range r.ipv4 {
|
|
||||||
url := fmt.Sprintf("https://%s/speedtest/test.rar/%f", domain, rand.Float64())
|
url := fmt.Sprintf("https://%s/speedtest/test.rar/%f", domain, rand.Float64())
|
||||||
|
|
||||||
var totalDuration float64
|
var totalDuration float64
|
||||||
hasError := false
|
var retErr error
|
||||||
for i := 0; i < iterations; i++ {
|
for i := 0; i < iterations; i++ {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), iterations*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.log.Warnf("Failed to create request for %s: %v", domain, err)
|
r.log.Warnf("Failed to create request for %s: %v", domain, err)
|
||||||
hasError = true
|
retErr = err
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,10 +142,10 @@ func (r *IPRepository) latencyTest(downloadClient *HTTPClient) {
|
|||||||
req.Header = headers
|
req.Header = headers
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
resp, err := downloadClient.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.log.Warnf("Failed to download from %s: %v", domain, err)
|
r.log.Warnf("Failed to download from %s: %v", domain, err)
|
||||||
hasError = true
|
retErr = err
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,21 +156,19 @@ func (r *IPRepository) latencyTest(downloadClient *HTTPClient) {
|
|||||||
|
|
||||||
if err != nil && err != io.EOF {
|
if err != nil && err != io.EOF {
|
||||||
r.log.Warnf("Failed to read from %s: %v", domain, err)
|
r.log.Warnf("Failed to read from %s: %v", domain, err)
|
||||||
hasError = true
|
retErr = err
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
duration := time.Since(start).Seconds()
|
duration := time.Since(start).Seconds()
|
||||||
totalDuration += duration
|
totalDuration += duration
|
||||||
}
|
}
|
||||||
if hasError {
|
if retErr != nil {
|
||||||
continue
|
return 0, retErr
|
||||||
}
|
}
|
||||||
|
|
||||||
r.latencyMap[domain] = totalDuration / 3
|
avgDuration := totalDuration / 3
|
||||||
|
return avgDuration, nil
|
||||||
r.log.Debugf("Latency from %s: %.5f seconds", domain, r.latencyMap[domain])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *IPRepository) readLatencyFile(latencyFile string) *map[string]float64 {
|
func (r *IPRepository) readLatencyFile(latencyFile string) *map[string]float64 {
|
||||||
@@ -172,16 +182,16 @@ func (r *IPRepository) readLatencyFile(latencyFile string) *map[string]float64 {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var latencyMap map[string]float64
|
var ipv4latencyMap map[string]float64
|
||||||
if err := json.Unmarshal(jsonData, &latencyMap); err != nil {
|
if err := json.Unmarshal(jsonData, &ipv4latencyMap); err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &latencyMap
|
return &ipv4latencyMap
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *IPRepository) writeLatencyFile(latencyFile string) {
|
func (r *IPRepository) writeLatencyFile(latencyFile string, data interface{}) {
|
||||||
file, err := os.Create(latencyFile)
|
file, err := os.Create(latencyFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.log.Warnf("Cannot create latency file %s: %v", latencyFile, err)
|
r.log.Warnf("Cannot create latency file %s: %v", latencyFile, err)
|
||||||
@@ -189,7 +199,7 @@ func (r *IPRepository) writeLatencyFile(latencyFile string) {
|
|||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
jsonData, err := json.Marshal(r.latencyMap)
|
jsonData, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.log.Warnf("Cannot marshal latency map: %v", err)
|
r.log.Warnf("Cannot marshal latency map: %v", err)
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user