Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
00c9f8d
feat: add `soaxreport` tool for ECH testing via SOAX proxies
jyyi1 Jan 20, 2026
32ed53a
Refactor SOAX client into `internal/soax`
jyyi1 Jan 21, 2026
1972fe7
Implement a reusable internal/curl package
jyyi1 Jan 21, 2026
ac756fb
implement performance stats collection via `curl -w`
jyyi1 Jan 27, 2026
55170b9
implement ECH testing with SOAX proxy
jyyi1 Jan 27, 2026
33397dd
Implement concurrent SOAX ECH testing
jyyi1 Jan 29, 2026
07a6740
Refactor to use CSV country list and include country names
jyyi1 Jan 29, 2026
d7f5a50
Ensure same exit node for one ISP thru sticky session.
jyyi1 Jan 29, 2026
e6fa052
Implement atomic progress tracking and simplify test func
jyyi1 Feb 2, 2026
e978f1e
Separate input ISP and exit node ISP header into different columns.
jyyi1 Feb 2, 2026
f0557b7
Update parallelism default value in README for soaxreport
jyyi1 Feb 4, 2026
1ade342
refactor(soax): rename BuildProxyURL to BuildWebProxyURL
jyyi1 Mar 10, 2026
679ca18
feat(soax): use outline-sdk for ISP retrieval
jyyi1 Mar 10, 2026
73a3023
fix(soax): use url.URL for proper escaping in BuildWebProxyURL
jyyi1 Mar 10, 2026
7b218ce
chore: add deduplication TODO for exit codes
jyyi1 Mar 10, 2026
31f28b8
refactor(echtest): extract runTest logic and remove translation layer
jyyi1 Mar 10, 2026
8c67c39
feat(soaxreport): set default path for countries list
jyyi1 Mar 10, 2026
80e9d6c
feat(soaxreport): save discovered ISPs to audit JSON file
jyyi1 Mar 10, 2026
70443e2
refactor(soax): remove Client wrapper and use environment variables f…
jyyi1 Mar 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,24 @@ go 1.24.8

require (
github.com/miekg/dns v1.1.70
golang.getoutline.org/sdk/x v0.1.0
golang.org/x/sync v0.19.0
)

require (
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
github.com/onsi/ginkgo/v2 v2.12.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.48.1 // indirect
go.uber.org/mock v0.4.0 // indirect
golang.getoutline.org/sdk v0.0.21 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.40.0 // indirect
)

Expand Down
56 changes: 56 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,14 +1,70 @@
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI=
github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.48.1 h1:y/8xmfWI9qmGTc+lBr4jKRUWLGSlSigv847ULJ4hYXA=
github.com/quic-go/quic-go v0.48.1/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28=
github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/things-go/go-socks5 v0.0.5 h1:qvKaGcBkfDrUL33SchHN93srAmYGzb4CxSM2DPYufe8=
github.com/things-go/go-socks5 v0.0.5/go.mod h1:mtzInf8v5xmsBpHZVbIw2YQYhc4K0jRwzfsH64Uh0IQ=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.getoutline.org/sdk v0.0.21 h1:zgtenz5DMbnIPOsuAOHNiWdrri81fHyBxhSfRi6Dk8s=
golang.getoutline.org/sdk v0.0.21/go.mod h1:raUAs4PYbEaT/cLTK6PviiKSh7gjEj7JJczFFFr41zc=
golang.getoutline.org/sdk/x v0.1.0 h1:8ykaCEC8Eoi3h/2MdGW7uaMAt2BWFCRhrSvuJ0Y/IU0=
golang.getoutline.org/sdk/x v0.1.0/go.mod h1:Vw7FWpLbYifHFYbbo0mXOCkhR14d1ADwjiF7uBQKyzM=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
1 change: 1 addition & 0 deletions greasereport/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type TestResult struct {
HTTPStatus int
}

// TODO: Deduplicate this with the unified ECH testing package (internal/echtest).
var curlExitCodeNames = map[int]string{
1: "CURLE_UNSUPPORTED_PROTOCOL",
2: "CURLE_FAILED_INIT",
Expand Down
242 changes: 242 additions & 0 deletions internal/echtest/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
// Copyright 2026 Google LLC
//
// 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
//
// https://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 echtest

import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
)

type TestResult struct {
Domain string
ECHGrease bool
Error string
CurlExitCode int
CurlErrorName string
DNSLookup time.Duration
TCPConnection time.Duration
TLSHandshake time.Duration
ServerTime time.Duration
TotalTime time.Duration
HTTPStatus int
Stderr string
}

// curlExitCodeNames maps curl exit codes to their CURL_* string representations.
var curlExitCodeNames = map[int]string{
1: "CURLE_UNSUPPORTED_PROTOCOL",
2: "CURLE_FAILED_INIT",
3: "CURLE_URL_MALFORMAT",
4: "CURLE_NOT_BUILT_IN",
5: "CURLE_COULDNT_RESOLVE_PROXY",
6: "CURLE_COULDNT_RESOLVE_HOST",
7: "CURLE_COULDNT_CONNECT",
8: "CURLE_WEIRD_SERVER_REPLY",
9: "CURLE_REMOTE_ACCESS_DENIED",
11: "CURLE_FTP_WEIRD_PASV_REPLY",
13: "CURLE_FTP_WEIRD_227_FORMAT",
14: "CURLE_FTP_CANT_GET_HOST",
15: "CURLE_FTP_CANT_RECONNECT",
17: "CURLE_FTP_COULDNT_SET_TYPE",
18: "CURLE_PARTIAL_FILE",
19: "CURLE_FTP_COULDNT_RETR_FILE",
21: "CURLE_QUOTE_ERROR",
22: "CURLE_HTTP_RETURNED_ERROR",
23: "CURLE_WRITE_ERROR",
25: "CURLE_UPLOAD_FAILED",
26: "CURLE_READ_ERROR",
27: "CURLE_OUT_OF_MEMORY",
28: "CURLE_OPERATION_TIMEDOUT",
30: "CURLE_FTP_PORT_FAILED",
31: "CURLE_FTP_COULDNT_USE_REST",
33: "CURLE_RANGE_ERROR",
34: "CURLE_HTTP_POST_ERROR",
35: "CURLE_SSL_CONNECT_ERROR",
36: "CURLE_BAD_DOWNLOAD_RESUME",
37: "CURLE_FILE_COULDNT_READ_FILE",
38: "CURLE_LDAP_CANNOT_BIND",
39: "CURLE_LDAP_SEARCH_FAILED",
41: "CURLE_FUNCTION_NOT_FOUND",
42: "CURLE_ABORTED_BY_CALLBACK",
43: "CURLE_BAD_FUNCTION_ARGUMENT",
45: "CURLE_INTERFACE_FAILED",
47: "CURLE_TOO_MANY_REDIRECTS",
48: "CURLE_UNKNOWN_OPTION",
49: "CURLE_TELNET_OPTION_SYNTAX",
51: "CURLE_PEER_FAILED_VERIFICATION",
52: "CURLE_GOT_NOTHING",
53: "CURLE_SSL_ENGINE_NOTFOUND",
54: "CURLE_SSL_ENGINE_SETFAILED",
55: "CURLE_SEND_ERROR",
56: "CURLE_RECV_ERROR",
58: "CURLE_SSL_CERTPROBLEM",
59: "CURLE_SSL_CIPHER",
60: "CURLE_SSL_CACERT",
61: "CURLE_BAD_CONTENT_ENCODING",
62: "CURLE_LDAP_INVALID_URL",
63: "CURLE_FILESIZE_EXCEEDED",
64: "CURLE_USE_SSL_FAILED",
65: "CURLE_SEND_FAIL_REWIND",
66: "CURLE_SSL_ENGINE_INITFAILED",
67: "CURLE_LOGIN_DENIED",
68: "CURLE_TFTP_NOTFOUND",
69: "CURLE_TFTP_PERM",
70: "CURLE_REMOTE_DISK_FULL",
71: "CURLE_TFTP_ILLEGAL",
72: "CURLE_TFTP_UNKNOWNID",
73: "CURLE_REMOTE_FILE_EXISTS",
74: "CURLE_TFTP_NOSUCHUSER",
75: "CURLE_CONV_FAILED",
76: "CURLE_CONV_REQD",
77: "CURLE_SSL_CACERT_BADFILE",
78: "CURLE_REMOTE_FILE_NOT_FOUND",
79: "CURLE_SSH",
80: "CURLE_SSL_SHUTDOWN_FAILED",
81: "CURLE_AGAIN",
82: "CURLE_SSL_CRL_BADFILE",
83: "CURLE_SSL_ISSUER_ERROR",
84: "CURLE_FTP_PRET_FAILED",
85: "CURLE_RTSP_CSEQ_ERROR",
86: "CURLE_RTSP_SESSION_ERROR",
87: "CURLE_FTP_BAD_FILE_LIST",
88: "CURLE_CHUNK_FAILED",
89: "CURLE_NO_CONNECTION_AVAILABLE",
90: "CURLE_SSL_PINNEDPUBKEYNOTMATCH",
91: "CURLE_SSL_INVALIDCERTSTATUS",
92: "CURLE_HTTP2_STREAM",
93: "CURLE_RECURSIVE_API_CALL",
94: "CURLE_AUTH_ERROR",
95: "CURLE_HTTP3",
96: "CURLE_QUIC_CONNECT_ERROR",
}

// Run executes a curl command against the specified domain.
func Run(
curlPath string,
domain string,
echGrease bool,
maxTime time.Duration,
proxyURL string,
proxyHeaders []string,
) TestResult {
result := TestResult{
Domain: domain,
ECHGrease: echGrease,
}

targetURL := "https://" + domain

args := []string{
"-w",
"dnslookup:%{time_namelookup},tcpconnect:%{time_connect},tlsconnect:%{time_appconnect},servertime:%{time_starttransfer},total:%{time_total},httpstatus:%{http_code}",
"--head",
"--max-time",
strconv.FormatFloat(maxTime.Seconds(), 'f', -1, 64),
}

// Handle proxy options
if proxyURL != "" {
args = append(args, "--proxy", proxyURL)
for _, h := range proxyHeaders {
args = append(args, "--proxy-header", h)
}
// If using a proxy with headers, we usually need verbose mode to see the proxy response.
// If proxy headers are provided, we assume the caller wants to read them from stderr.
if len(proxyHeaders) > 0 {
args = append(args, "-v")
} else {
args = append(args, "-s")
}
} else {
args = append(args, "-s")
}

if echGrease {
args = append(args, "--ech", "grease")
} else {
args = append(args, "--ech", "false")
}
args = append(args, targetURL)

cmd := exec.Command(curlPath, args...)

// Setup environment for custom curl (matching internal/curl/runner.go)
binDir := filepath.Dir(curlPath)
libDir := filepath.Join(filepath.Dir(binDir), "lib")
if libStat, err := os.Stat(libDir); err == nil && libStat.IsDir() {
cmd.Env = append(os.Environ(), "LD_LIBRARY_PATH="+libDir)
}

var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr

err := cmd.Run()
result.Stderr = stderr.String() // Always capture stderr for caller

if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
result.CurlExitCode = exitError.ExitCode()
result.CurlErrorName = curlExitCodeNames[result.CurlExitCode]
} else {
result.Error = fmt.Sprintf("failed to execute curl: %v", err)
return result
}
} else {
// Even if err is nil, there might be curl-level errors recorded in stderr
// that the caller might be interested in, though standard execution succeeded.
}

// parse the stdout stats
parts := strings.SplitSeq(stdout.String(), ",")
for part := range parts {
kv := strings.Split(part, ":")
if len(kv) != 2 {
continue
}
key := kv[0]
value := kv[1]

switch key {
case "dnslookup":
f, _ := strconv.ParseFloat(value, 64)
result.DNSLookup = time.Duration(f * float64(time.Second))
case "tcpconnect":
f, _ := strconv.ParseFloat(value, 64)
result.TCPConnection = time.Duration(f * float64(time.Second))
case "tlsconnect":
f, _ := strconv.ParseFloat(value, 64)
result.TLSHandshake = time.Duration(f * float64(time.Second))
case "servertime":
f, _ := strconv.ParseFloat(value, 64)
result.ServerTime = time.Duration(f * float64(time.Second))
case "total":
f, _ := strconv.ParseFloat(value, 64)
result.TotalTime = time.Duration(f * float64(time.Second))
case "httpstatus":
i, _ := strconv.Atoi(value)
result.HTTPStatus = i
}
}

return result
}
Loading