Repository: ekalinin/awsping Branch: master Commit: 31ac8c5f70aa Files: 19 Total size: 34.2 KB Directory structure: gitextract_d_wjxg93/ ├── .dockerignore ├── .github/ │ └── workflows/ │ ├── goreleaser.yml │ └── pr.yml ├── .gitignore ├── .goreleaser.yml ├── Dockerfile ├── Dockerfile.goreleaser ├── LICENSE ├── Makefile ├── README.md ├── aws.go ├── aws_test.go ├── cmd/ │ └── awsping/ │ └── main.go ├── go.mod ├── request.go ├── request_test.go ├── target.go ├── utils.go └── utils_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ .git LICENSE README.md Dockerfile awsping ================================================ FILE: .github/workflows/goreleaser.yml ================================================ name: goreleaser on: push: branches: - "!*" tags: - "v*.*.*" jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.17 - name: Login to Docker Hub uses: docker/login-action@v1 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} - name: Login to GitHub Container Registry uses: docker/login-action@v1 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GH_PACKAGE_TOKEN }} - name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 with: version: latest args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/pr.yml ================================================ name: pr/push checks on: push: branches: - master paths-ignore: - '**/*.md' - 'Makefile' pull_request: branches: - master paths-ignore: - '**/*.md' - 'Makefile' jobs: build: name: Build, Test, Coverage runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.17 - name: Lint uses: golangci/golangci-lint-action@v2 - name: Build run: go build -v ./... - name: Test & Coverage run: go test -v -coverprofile=coverage.out -covermode=atomic - name: Upload coverage to Codecov run: bash <(curl -s https://codecov.io/bash) ================================================ FILE: .gitignore ================================================ *.swp TODO.md dist/ # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof coverage.out ================================================ FILE: .goreleaser.yml ================================================ project_name: awsping builds: - env: - CGO_ENABLED=0 main: ./cmd/awsping goos: - linux - windows - darwin goarch: - amd64 checksum: name_template: 'checksums.txt' snapshot: name_template: "{{ .Tag }}-next" changelog: sort: asc filters: exclude: - '^docs:' - '^test:' - '^Makefile:' - '^README:' - '^gitignore:' - '^goreleaser:' dockers: - image_templates: - "docker.io/evkalinin/{{.ProjectName}}:{{ .Tag }}" - "docker.io/evkalinin/{{.ProjectName}}:latest" - "ghcr.io/ekalinin/{{.ProjectName}}:{{ .Tag }}" - "ghcr.io/ekalinin/{{.ProjectName}}:latest" dockerfile: Dockerfile.goreleaser build_flag_templates: - "--pull" - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.name={{.ProjectName}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.source={{.GitURL}}" - "--platform=linux/amd64" ================================================ FILE: Dockerfile ================================================ FROM golang:1.17-bullseye as build COPY . /build WORKDIR /build RUN make FROM gcr.io/distroless/base COPY --from=build /build/awsping / ENTRYPOINT ["/awsping"] ================================================ FILE: Dockerfile.goreleaser ================================================ FROM gcr.io/distroless/base COPY awsping / ENTRYPOINT ["/awsping"] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2016 Eugene Kalinin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ NAME=awsping EXEC=${NAME} BUILD_DIR=build BUILD_OS="windows darwin freebsd linux" BUILD_ARCH="amd64 386" BUILD_DIR=build SRC_CMD=cmd/awsping/main.go VERSION=`grep "Version" utils.go | grep -o -E '[0-9]\.[0-9]\.[0-9]{1,2}'` build: go build -race -o ${EXEC} ${SRC_CMD} clean: @rm -f ${EXEC} @rm -f ${BUILD_DIR}/* @go clean # # Tests, linters # lint: golint # make run ARGS="-h" run: go run cmd/awsping/main.go $(ARGS) test: lint @go test -cover . # # Release # check-version: ifdef VERSION @echo Current version: $(VERSION) else $(error VERSION is not set) endif check-master: ifneq ($(shell git rev-parse --abbrev-ref HEAD),master) $(error You're not on the "master" branch) endif release: check-master check-version git tag v${VERSION} && \ git push origin v${VERSION} release-test: check-master check-version goreleaser release --snapshot --rm-dist buildall: clean @mkdir -p ${BUILD_DIR} @for os in "${BUILD_OS}" ; do \ for arch in "${BUILD_ARCH}" ; do \ echo " * build $$os for $$arch"; \ GOOS=$$os GOARCH=$$arch go build -ldflags "-s" -o ${BUILD_DIR}/${EXEC} ${SRC_CMD}; \ cd ${BUILD_DIR}; \ tar czf ${EXEC}.$$os.$$arch.tgz ${EXEC}; \ cd - ; \ done done @rm ${BUILD_DIR}/${EXEC} # # Docker # docker: docker build -t awsping . docker-run: docker docker run awsping -verbose 2 -repeats 2 ================================================ FILE: README.md ================================================ # awsping Console tool to check the latency to each AWS region [![Go Report Card](https://goreportcard.com/badge/github.com/ekalinin/awsping)](https://goreportcard.com/report/github.com/ekalinin/awsping) [![codecov](https://codecov.io/gh/ekalinin/awsping/branch/master/graph/badge.svg)](https://codecov.io/gh/ekalinin/awsping) [![Go Reference](https://pkg.go.dev/badge/github.com/ekalinin/awsping.svg)](https://pkg.go.dev/github.com/ekalinin/awsping) [![MIT license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/ekalinin/awsping) # ToC * [Usage](#usage) * [Test via TCP](#test-via-tcp) * [Test via HTTP](#test-via-http) * [Test via HTTPS](#test-via-https) * [Test several times](#test-several-times) * [Verbose mode](#verbose-mode) * [Get Help](#get-help) * [Get binary file](#get-binary-file) * [Build from sources](#build-from-sources) * [Use with Docker](#use-with-docker) * [Build a Docker image](#build-a-docker-image) * [Run the Docker image](#run-the-docker-image) # Usage ## Test via TCP ```bash ➥ ./awsping Europe (Frankfurt) 51.86 ms Europe (Ireland) 62.86 ms US-East (Virginia) 126.39 ms US-East (Ohio) 154.81 ms Asia Pacific (Mumbai) 181.09 ms US-West (California) 194.27 ms US-West (Oregon) 211.87 ms South America (São Paulo) 246.20 ms Asia Pacific (Tokyo) 309.27 ms Asia Pacific (Seoul) 322.76 ms Asia Pacific (Sydney) 346.37 ms Asia Pacific (Singapore) 407.91 ms ``` ## Test via HTTP ```bash ➥ ./awsping -http Europe (Frankfurt) 222.56 ms Europe (Ireland) 226.76 ms US-East (Virginia) 349.17 ms US-West (California) 488.12 ms US-East (Ohio) 513.69 ms Asia Pacific (Mumbai) 528.51 ms US-West (Oregon) 532.05 ms South America (São Paulo) 599.36 ms Asia Pacific (Seoul) 715.92 ms Asia Pacific (Sydney) 721.47 ms Asia Pacific (Tokyo) 745.24 ms Asia Pacific (Singapore) 847.36 ms ``` ## Test via HTTPS ```bash ➥ ./awsping -https Europe (Stockholm) 216.67 ms Europe (Frankfurt) 263.20 ms Europe (Paris) 284.32 ms Europe (Milan) 305.63 ms Europe (Ireland) 327.34 ms Europe (London) 332.17 ms Middle East (Bahrain) 590.74 ms US-East (N. Virginia) 595.13 ms Canada (Central) 628.44 ms US-East (Ohio) 635.32 ms Asia Pacific (Mumbai) 755.56 ms Asia Pacific (Hong Kong) 843.90 ms US-West (N. California) 870.65 ms Asia Pacific (Singapore) 899.50 ms Africa (Cape Town) 912.06 ms US-West (Oregon) 919.34 ms South America (São Paulo) 985.93 ms Asia Pacific (Tokyo) 1122.67 ms Asia Pacific (Seoul) 1138.76 ms Asia Pacific (Osaka) 1167.40 ms Asia Pacific (Sydney) 1328.90 ms ``` ## Test several times ```bash ➥ ./awsping -repeats 3 Europe (Frankfurt) 50.13 ms Europe (Ireland) 62.67 ms US-East (Virginia) 126.88 ms US-East (Ohio) 155.37 ms US-West (California) 195.75 ms US-West (Oregon) 206.19 ms Asia Pacific (Mumbai) 222.34 ms South America (São Paulo) 254.28 ms Asia Pacific (Tokyo) 308.52 ms Asia Pacific (Seoul) 325.93 ms Asia Pacific (Sydney) 349.62 ms Asia Pacific (Singapore) 378.53 ms ``` ## Verbose mode ```bash ➥ ./awsping -repeats 3 -verbose 1 Code Region Latency 0 eu-central-1 Europe (Frankfurt) 47.39 ms 1 eu-west-1 Europe (Ireland) 62.28 ms 2 us-east-1 US-East (Virginia) 128.45 ms 3 us-east-2 US-East (Ohio) 155.53 ms 4 us-west-1 US-West (California) 194.37 ms 5 us-west-2 US-West (Oregon) 208.91 ms 6 ap-south-1 Asia Pacific (Mumbai) 226.59 ms 7 sa-east-1 South America (São Paulo) 254.67 ms 8 ap-northeast-1 Asia Pacific (Tokyo) 301.97 ms 9 ap-northeast-2 Asia Pacific (Seoul) 323.10 ms 10 ap-southeast-2 Asia Pacific (Sydney) 341.26 ms 11 ap-southeast-1 Asia Pacific (Singapore) 397.47 ms ``` ```bash ➥ ./awsping -repeats 3 -verbose 2 Code Region Try #1 Try #2 Try #3 Avg Latency 0 eu-central-1 Europe (Frankfurt) 45.18 ms 45.46 ms 45.68 ms 45.44 ms 1 eu-west-1 Europe (Ireland) 61.89 ms 62.99 ms 62.98 ms 62.62 ms 2 us-east-1 US-East (Virginia) 125.15 ms 126.75 ms 126.49 ms 126.13 ms 3 us-east-2 US-East (Ohio) 154.05 ms 154.28 ms 153.53 ms 153.96 ms 4 us-west-1 US-West (California) 196.20 ms 195.05 ms 193.76 ms 195.00 ms 5 us-west-2 US-West (Oregon) 204.04 ms 203.97 ms 203.84 ms 203.95 ms 6 ap-south-1 Asia Pacific (Mumbai) 175.27 ms 300.68 ms 172.18 ms 216.05 ms 7 sa-east-1 South America (São Paulo) 243.48 ms 247.12 ms 248.32 ms 246.31 ms 8 ap-northeast-1 Asia Pacific (Tokyo) 324.78 ms 312.70 ms 319.02 ms 318.83 ms 9 ap-northeast-2 Asia Pacific (Seoul) 328.96 ms 327.65 ms 326.17 ms 327.59 ms 10 ap-southeast-2 Asia Pacific (Sydney) 388.17 ms 347.74 ms 393.58 ms 376.50 ms 11 ap-southeast-1 Asia Pacific (Singapore) 409.53 ms 403.61 ms 405.84 ms 406.33 ms ``` ## Get Help ```bash ➜ ./awsping -h Usage of ./awsping: -http Use http transport (default is tcp) -https Use https transport (default is tcp) -list-regions Show list of regions -repeats int Number of repeats (default 1) -service string AWS Service: ec2, sdb, sns, sqs, ... (default "dynamodb") -v Show version -verbose int Verbosity level ``` # Get binary file ```bash $ wget https://github.com/ekalinin/awsping/releases/download/0.5.2/awsping.linux.amd64.tgz $ tar xzvf awsping.linux.amd64.tgz $ chmod +x awsping $ ./awsping -v 0.5.2 ``` # Build from sources ```bash ➥ make build ``` # Use with Docker ## Build a Docker image ``` $ docker build -t awsping . ``` ## Run the Docker image ``` $ docker run --rm awsping ``` Arguments can be used as mentioned in the _Usage_ section. i.e.: ``` $ docker run --rm awsping -repeats 3 -verbose 2 ``` ================================================ FILE: aws.go ================================================ package awsping import ( "fmt" "sync" "time" ) // CheckType describes a type for a check type CheckType int const ( // CheckTypeTCP is TCP type of check CheckTypeTCP CheckType = iota // CheckTypeHTTP is HTTP type of check CheckTypeHTTP // CheckTypeHTTPS is HTTPS type of check CheckTypeHTTPS ) // -------------------------------------------- // AWSRegion description of the AWS EC2 region type AWSRegion struct { Name string Code string Service string Latencies []time.Duration Error error CheckType CheckType Target Targetter Request Requester } // NewRegion creates a new region with a name and code func NewRegion(name, code string) AWSRegion { return AWSRegion{ Name: name, Code: code, CheckType: CheckTypeTCP, Request: NewAWSRequest(), } } // CheckLatency does a latency check for a region func (r *AWSRegion) CheckLatency(wg *sync.WaitGroup) { defer wg.Done() if r.CheckType == CheckTypeHTTP || r.CheckType == CheckTypeHTTPS { r.checkLatencyHTTP(r.CheckType == CheckTypeHTTPS) } else { r.checkLatencyTCP() } } // checkLatencyHTTP Test Latency via HTTP func (r *AWSRegion) checkLatencyHTTP(https bool) { url := r.Target.GetURL() l, err := r.Request.Do(useragent, url, RequestTypeHTTP) if err != nil { r.Error = err return } r.Latencies = append(r.Latencies, l) } // checkLatencyTCP Test Latency via TCP func (r *AWSRegion) checkLatencyTCP() { tcpAddr, err := r.Target.GetIP() if err != nil { r.Error = err return } l, err := r.Request.Do(useragent, tcpAddr.String(), RequestTypeTCP) if err != nil { r.Error = err return } r.Latencies = append(r.Latencies, l) } // GetLatency returns Latency in ms func (r *AWSRegion) GetLatency() float64 { sum := float64(0) for _, l := range r.Latencies { sum += Duration2ms(l) } return sum / float64(len(r.Latencies)) } // GetLatencyStr returns Latency in string func (r *AWSRegion) GetLatencyStr() string { if r.Error != nil { return r.Error.Error() } return fmt.Sprintf("%.2f ms", r.GetLatency()) } // -------------------------------------------- // AWSRegions slice of the AWSRegion type AWSRegions []AWSRegion // Len returns a count of regions func (rs AWSRegions) Len() int { return len(rs) } // Less return a result of latency compare between two regions func (rs AWSRegions) Less(i, j int) bool { return rs[i].GetLatency() < rs[j].GetLatency() } // Swap two regions by index func (rs AWSRegions) Swap(i, j int) { rs[i], rs[j] = rs[j], rs[i] } // SetService sets service for all regions func (rs AWSRegions) SetService(service string) { for i := range rs { rs[i].Service = service } } // SetCheckType sets Check Type for all regions func (rs AWSRegions) SetCheckType(checkType CheckType) { for i := range rs { rs[i].CheckType = checkType } } // SetDefaultTarget sets default target instance func (rs AWSRegions) SetDefaultTarget() { rs.SetTarget(func(r *AWSRegion) { r.Target = &AWSTarget{ HTTPS: r.CheckType == CheckTypeHTTPS, Code: r.Code, Service: r.Service, Rnd: mkRandomString(13), } }) } // SetTarget sets default target instance for all regions func (rs AWSRegions) SetTarget(fn func(r *AWSRegion)) { for i := range rs { fn(&rs[i]) } } ================================================ FILE: aws_test.go ================================================ package awsping import ( "errors" "fmt" "net" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" ) func TestAWSRegionError(t *testing.T) { AWSErr := errors.New("something bad") r := AWSRegion{Error: AWSErr} got := r.GetLatencyStr() want := AWSErr.Error() if got != want { t.Errorf("failed:\ngot=%q\nwant=%q", got, want) } } type testTarget struct { URL string IP *net.TCPAddr } func (r *testTarget) GetURL() string { return r.URL } // GetIP return IP for AWS target func (r *testTarget) GetIP() (*net.TCPAddr, error) { return r.IP, nil } func TestAWSRegionCheckLatencyHTTP(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(15 * time.Millisecond) fmt.Fprintln(w, "X") })) defer ts.Close() tt := testTarget{URL: ts.URL} regions := GetRegions() service := "ec2" checkType := CheckTypeHTTP regions.SetService(service) regions.SetCheckType(checkType) regions.SetTarget(func(r *AWSRegion) { r.Target = &tt }) var wg sync.WaitGroup wg.Add(1) regions[0].CheckLatency(&wg) got := regions[0].GetLatency() want := 15.0 if got < want || got > want*2 { t.Errorf("failed:\ngot=%f\nwant=%f", got, want) } // check "error" errTxt := "something bad" regions[0].Request = &testRequest{err: errors.New(errTxt)} wg.Add(1) regions[0].CheckLatency(&wg) if regions[0].Error == nil { t.Errorf("failed: error should not be empty") } if regions[0].Error.Error() != errTxt { t.Errorf("failed: error should be empty=%s", errTxt) } } type testRequest struct { duration time.Duration err error } func (d *testRequest) Do(_, _ string, _ RequestType) (time.Duration, error) { if d.err != nil { return 0, d.err } return d.duration, nil } func TestAWSRegionCheckLatencyTCP(t *testing.T) { // just random local IP tt := testTarget{IP: &net.TCPAddr{ IP: net.IPv4(127, 0, 0, 1), Port: 67890, }} regions := GetRegions() service := "ec2" checkType := CheckTypeTCP regions.SetService(service) regions.SetCheckType(checkType) regions.SetTarget(func(r *AWSRegion) { r.Target = &tt }) regions[0].Request = &testRequest{duration: 15 * time.Millisecond} var wg sync.WaitGroup wg.Add(1) regions[0].CheckLatency(&wg) got := regions[0].GetLatency() want := 15.0 if got < want || got > want+1 { t.Errorf("failed:\ngot=%f\nwant=%f\nregion=%q", got, want, regions[0]) } if regions[0].Error != nil { t.Errorf("failed: error should be empty") } // check "error" errTxt := "something bad" regions[0].Request = &testRequest{err: errors.New(errTxt)} wg.Add(1) regions[0].CheckLatency(&wg) if regions[0].Error == nil { t.Errorf("failed: error should not be empty") } if regions[0].Error.Error() != errTxt { t.Errorf("failed: error should be empty=%s", errTxt) } } // --------------------------------------------- func TestAWSRegionsLen(t *testing.T) { regions := GetRegions() got := regions.Len() want := len(regions) if got != want { t.Errorf("failed:\ngot=%q\nwant=%q", got, want) } } func TestAWSRegionsLess(t *testing.T) { regions := GetRegions() regions[0].Latencies = []time.Duration{15 * time.Millisecond} regions[1].Latencies = []time.Duration{25 * time.Millisecond} if !regions.Less(0, 1) { t.Errorf("failed: not less, regions=%q", regions) } } func TestAWSRegionsSwap(t *testing.T) { regions := GetRegions() regions[0].Latencies = []time.Duration{15 * time.Millisecond} regions[1].Latencies = []time.Duration{25 * time.Millisecond} regions.Swap(0, 3) if len(regions[0].Latencies) != 0 { t.Errorf("failed: not swapped, regions=%q", regions) } } func TestAWSRegionsSetService(t *testing.T) { regions := GetRegions() service := "ec2" regions.SetService(service) if regions[0].Service != service || regions[len(regions)-1].Service != service { t.Errorf("failed: not set, regions=%q, service=%s", regions, service) } } func TestAWSRegionsSetCheckType(t *testing.T) { regions := GetRegions() checkType := CheckTypeHTTP regions.SetCheckType(checkType) if regions[0].CheckType != checkType || regions[len(regions)-1].CheckType != checkType { t.Errorf("failed: not set, regions=%q, checkType=%d", regions, checkType) } } func TestAWSRegionsSetDefaultTarget(t *testing.T) { regions := GetRegions() service := "ec2" checkType := CheckTypeHTTPS regions.SetService(service) regions.SetCheckType(checkType) regions.SetDefaultTarget() got := regions[0].Target.GetURL() want := fmt.Sprintf("https://ec2.%s.amazonaws.com/ping?x=", regions[0].Code) if !strings.HasPrefix(got, want) { t.Errorf("failed: wrong url\ngot=%s\nneed=%s", got, want) } } ================================================ FILE: cmd/awsping/main.go ================================================ package main import ( "flag" "fmt" "math/rand" "os" "time" "github.com/ekalinin/awsping" ) var ( repeats = flag.Int("repeats", 1, "Number of repeats") useHTTP = flag.Bool("http", false, "Use http transport (default is tcp)") useHTTPS = flag.Bool("https", false, "Use https transport (default is tcp)") showVer = flag.Bool("v", false, "Show version") verbose = flag.Int("verbose", 0, "Verbosity level (0: name-latency); 1: code-name-latency; 2: code-name-tries-avg") service = flag.String("service", "dynamodb", "AWS Service: ec2, sdb, sns, sqs, ...") listRegions = flag.Bool("list-regions", false, "Show list of regions") ) func main() { flag.Parse() if *showVer { fmt.Println(awsping.Version) os.Exit(0) } regions := awsping.GetRegions() if *listRegions { lo := awsping.NewOutput(awsping.ShowOnlyRegions, 0) lo.Show(®ions) os.Exit(0) } rand.Seed(time.Now().UnixNano()) awsping.CalcLatency(regions, *repeats, *useHTTP, *useHTTPS, *service) lo := awsping.NewOutput(*verbose, *repeats) lo.Show(®ions) } ================================================ FILE: go.mod ================================================ module github.com/ekalinin/awsping go 1.17 ================================================ FILE: request.go ================================================ package awsping import ( "net" "net/http" "time" ) // RequestType describes a type for a request type type RequestType int const ( // RequestTypeHTTP is HTTP type of request RequestTypeHTTP RequestType = iota // RequestTypeTCP is TCP type of request RequestTypeTCP ) // Requester is an interface to do a network request type Requester interface { Do(ua, url string, reqType RequestType) (time.Duration, error) } // AWSHTTPRequester is an interface for HTTP requests type AWSHTTPRequester interface { Do(req *http.Request) (*http.Response, error) } // AWSTCPRequester is an interface for TCP requests type AWSTCPRequester interface { Dial(network, address string) (net.Conn, error) } // AWSRequest implements Requester interface type AWSRequest struct { httpClient AWSHTTPRequester tcpClient AWSTCPRequester } // NewAWSRequest creates a new instance of AWSRequest func NewAWSRequest() *AWSRequest { return &AWSRequest{ httpClient: &http.Client{}, tcpClient: &net.Dialer{}, } } // DoHTTP does HTTP request for a URL by User-Agent (ua) func (r *AWSRequest) DoHTTP(ua, url string) (time.Duration, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { return 0, err } req.Header.Set("User-Agent", ua) start := time.Now() resp, err := r.httpClient.Do(req) latency := time.Since(start) if err != nil { return 0, err } defer resp.Body.Close() return latency, nil } // DoTCP does TCP request to the Addr func (r *AWSRequest) DoTCP(_, addr string) (time.Duration, error) { start := time.Now() conn, err := r.tcpClient.Dial("tcp", addr) if err != nil { return 0, err } l := time.Since(start) defer conn.Close() return l, nil } // Do does a request. Type of request depends on reqType func (r *AWSRequest) Do(ua, url string, reqType RequestType) (time.Duration, error) { if reqType == RequestTypeHTTP { return r.DoHTTP(ua, url) } return r.DoTCP(ua, url) } ================================================ FILE: request_test.go ================================================ package awsping import ( "errors" "net" "net/http" "testing" ) type testTCPClient struct { err error } func (c *testTCPClient) Dial(n, a string) (net.Conn, error) { if c.err != nil { return nil, c.err } var con net.Conn return con, nil } type testHTTPClient struct { err error } func (c *testHTTPClient) Do(r *http.Request) (*http.Response, error) { if c.err != nil { return nil, c.err } return &http.Response{}, nil } func TestRequestDoTCPError(t *testing.T) { r := &AWSRequest{ tcpClient: &testTCPClient{ err: net.ErrWriteToConnected, }, } l, err := r.DoTCP("net", "some-addr") if err == nil { t.Errorf("Error should not be empty") } if !errors.Is(err, net.ErrWriteToConnected) { t.Errorf("Want=%v, got=%v", net.ErrWriteToConnected, err) } if l != 0 { t.Errorf("Latency for error should be 0, but got=%d", l) } } func TestDoErr(t *testing.T) { errTCP := errors.New("error from tcp") errHTTP := errors.New("error from http") r := &AWSRequest{ tcpClient: &testTCPClient{ err: errTCP, }, httpClient: &testHTTPClient{ err: errHTTP, }, } l, err := r.Do("ua", "addr", RequestTypeTCP) if err == nil { t.Errorf("Error should not be empty") } if !errors.Is(err, errTCP) { t.Errorf("Want=%v, got=%v", errTCP, err) } if l != 0 { t.Errorf("Latency for error should be 0, but got=%d", l) } l, err = r.Do("ua", "addr", RequestTypeHTTP) if err == nil { t.Errorf("Error should not be empty") } if !errors.Is(err, errHTTP) { t.Errorf("Want=%v, got=%v", errHTTP, err) } if l != 0 { t.Errorf("Latency for error should be 0, but got=%d", l) } } ================================================ FILE: target.go ================================================ package awsping import ( "fmt" "net" ) // Targetter is an interface to get target's IP or URL type Targetter interface { GetURL() string GetIP() (*net.TCPAddr, error) } // AWSTarget implements Targetter for AWS type AWSTarget struct { HTTPS bool Code string Service string Rnd string } // GetURL return URL for AWS target func (r *AWSTarget) GetURL() string { proto := "http" if r.HTTPS { proto = "https" } hostname := fmt.Sprintf("%s.%s.amazonaws.com", r.Service, r.Code) url := fmt.Sprintf("%s://%s/ping?x=%s", proto, hostname, r.Rnd) return url } // GetIP return IP for AWS target func (r *AWSTarget) GetIP() (*net.TCPAddr, error) { tcpURI := fmt.Sprintf("%s.%s.amazonaws.com:80", r.Service, r.Code) return net.ResolveTCPAddr("tcp4", tcpURI) } ================================================ FILE: utils.go ================================================ package awsping import ( "fmt" "io" "math/rand" "os" "sort" "strconv" "strings" "sync" "time" ) var ( // Version describes application version Version = "2.0.0" github = "https://github.com/ekalinin/awsping" useragent = fmt.Sprintf("AwsPing/%s (+%s)", Version, github) ) const ( // ShowOnlyRegions describes a type of output when only region's name and code printed out ShowOnlyRegions = -1 ) var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") // Duration2ms converts time.Duration to ms (float64) func Duration2ms(d time.Duration) float64 { return float64(d.Nanoseconds()) / 1000 / 1000 } // mkRandomString returns random string func mkRandomString(n int) string { b := make([]rune, n) for i := range b { b[i] = letterRunes[rand.Intn(len(letterRunes))] } return string(b) } // LatencyOutput prints data into console type LatencyOutput struct { Level int Repeats int w io.Writer } // NewOutput creates a new LatencyOutput instance func NewOutput(level, repeats int) *LatencyOutput { return &LatencyOutput{ Level: level, Repeats: repeats, w: os.Stdout, } } func (lo *LatencyOutput) show(regions *AWSRegions) { for _, r := range *regions { fmt.Fprintf(lo.w, "%-15s %-s\n", r.Code, r.Name) } } func (lo *LatencyOutput) show0(regions *AWSRegions) { for _, r := range *regions { fmt.Fprintf(lo.w, "%-25s %20s\n", r.Name, r.GetLatencyStr()) } } func (lo *LatencyOutput) show1(regions *AWSRegions) { outFmt := "%5v %-15s %-30s %20s\n" fmt.Fprintf(lo.w, outFmt, "", "Code", "Region", "Latency") for i, r := range *regions { fmt.Fprintf(lo.w, outFmt, i, r.Code, r.Name, r.GetLatencyStr()) } } func (lo *LatencyOutput) show2(regions *AWSRegions) { // format outFmt := "%5v %-15s %-25s" outFmt += strings.Repeat(" %15s", lo.Repeats) + " %15s\n" // header outStr := []interface{}{"", "Code", "Region"} for i := 0; i < lo.Repeats; i++ { outStr = append(outStr, "Try #"+strconv.Itoa(i+1)) } outStr = append(outStr, "Avg Latency") // show header fmt.Fprintf(lo.w, outFmt, outStr...) // each region stats for i, r := range *regions { outData := []interface{}{strconv.Itoa(i), r.Code, r.Name} for n := 0; n < lo.Repeats; n++ { outData = append(outData, fmt.Sprintf("%.2f ms", Duration2ms(r.Latencies[n]))) } outData = append(outData, fmt.Sprintf("%.2f ms", r.GetLatency())) fmt.Fprintf(lo.w, outFmt, outData...) } } // Show print data func (lo *LatencyOutput) Show(regions *AWSRegions) { switch lo.Level { case ShowOnlyRegions: lo.show(regions) case 0: lo.show0(regions) case 1: lo.show1(regions) case 2: lo.show2(regions) } } // GetRegions returns a list of regions func GetRegions() AWSRegions { return AWSRegions{ NewRegion("Africa (Cape Town)", "af-south-1"), NewRegion("Asia Pacific (Hong Kong)", "ap-east-1"), NewRegion("Asia Pacific (Tokyo)", "ap-northeast-1"), NewRegion("Asia Pacific (Seoul)", "ap-northeast-2"), NewRegion("Asia Pacific (Osaka)", "ap-northeast-3"), NewRegion("Asia Pacific (Mumbai)", "ap-south-1"), NewRegion("Asia Pacific (Hyderabad)", "ap-south-2"), NewRegion("Asia Pacific (Singapore)", "ap-southeast-1"), NewRegion("Asia Pacific (Sydney)", "ap-southeast-2"), NewRegion("Asia Pacific (Jakarta)", "ap-southeast-3"), NewRegion("Asia Pacific (Melbourne)", "ap-southeast-4"), NewRegion("Canada (Central)", "ca-central-1"), NewRegion("Europe (Frankfurt)", "eu-central-1"), NewRegion("Europe (Zurich)", "eu-central-2"), NewRegion("Europe (Stockholm)", "eu-north-1"), NewRegion("Europe (Milan)", "eu-south-1"), NewRegion("Europe (Spain)", "eu-south-2"), NewRegion("Europe (Ireland)", "eu-west-1"), NewRegion("Europe (London)", "eu-west-2"), NewRegion("Europe (Paris)", "eu-west-3"), NewRegion("Middle East (UAE)", "me-central-1"), NewRegion("Middle East (Bahrain)", "me-south-1"), NewRegion("South America (Sao Paulo)", "sa-east-1"), NewRegion("US East (N. Virginia)", "us-east-1"), NewRegion("US East (Ohio)", "us-east-2"), NewRegion("US West (N. California)", "us-west-1"), NewRegion("US West (Oregon)", "us-west-2"), NewRegion("Israel (Tel Aviv)", "il-central-1"), } } // CalcLatency returns list of aws regions sorted by Latency func CalcLatency(regions AWSRegions, repeats int, useHTTP bool, useHTTPS bool, service string) { regions.SetService(service) switch { case useHTTP: regions.SetCheckType(CheckTypeHTTP) case useHTTPS: regions.SetCheckType(CheckTypeHTTPS) default: regions.SetCheckType(CheckTypeTCP) } regions.SetDefaultTarget() var wg sync.WaitGroup for n := 1; n <= repeats; n++ { wg.Add(len(regions)) for i := range regions { go regions[i].CheckLatency(&wg) } wg.Wait() } sort.Sort(regions) } ================================================ FILE: utils_test.go ================================================ package awsping import ( "bytes" "testing" "time" ) func TestDuration(t *testing.T) { input := time.Duration(1 * time.Second) want := 1000.0 got := Duration2ms(input) if got != want { t.Errorf("Duration was incorrect, got: %f, want: %f.", got, want) } } func TestRandomString(t *testing.T) { tests := []struct { n int res string }{ {1, ""}, {5, ""}, {10, ""}, } for idx, test := range tests { test.res = mkRandomString(test.n) if len(test.res) != test.n { t.Errorf("Try %d: n=%d, got: %s (len=%d), want: %d.", idx, test.n, test.res, len(test.res), test.n) } } } func TestOutputShowOnlyRegions(t *testing.T) { var b bytes.Buffer lo := NewOutput(ShowOnlyRegions, 0) lo.w = &b regions := GetRegions()[:2] lo.Show(®ions) got := b.String() want := "af-south-1 Africa (Cape Town)\n" + "ap-east-1 Asia Pacific (Hong Kong)\n" if got != want { t.Errorf("Show:\ngot =%q\nwant=%q", got, want) } } func TestOutputShow0(t *testing.T) { var b bytes.Buffer lo := NewOutput(0, 0) lo.w = &b regions := GetRegions()[:2] regions[0].Latencies = []time.Duration{15 * time.Millisecond} regions[1].Latencies = []time.Duration{25 * time.Millisecond} lo.Show(®ions) want := "Africa (Cape Town) 15.00 ms\n" + "Asia Pacific (Hong Kong) 25.00 ms\n" got := b.String() if got != want { t.Errorf("Show0 failed:\ngot =%q\nwant=%q", got, want) } } func TestOutputShow1(t *testing.T) { var b bytes.Buffer lo := NewOutput(1, 0) lo.w = &b regions := GetRegions()[:2] regions[0].Latencies = []time.Duration{15 * time.Millisecond} regions[1].Latencies = []time.Duration{25 * time.Millisecond} lo.Show(®ions) got := b.String() want := " Code Region Latency\n" + " 0 af-south-1 Africa (Cape Town) 15.00 ms\n" + " 1 ap-east-1 Asia Pacific (Hong Kong) 25.00 ms\n" if got != want { t.Errorf("Show1 failed:\ngot =%q\nwant=%q", got, want) } } func TestOutputShow2(t *testing.T) { var b bytes.Buffer lo := NewOutput(2, 2) lo.w = &b regions := GetRegions()[:2] regions[0].Latencies = []time.Duration{15 * time.Millisecond, 17 * time.Millisecond} regions[1].Latencies = []time.Duration{25 * time.Millisecond, 26 * time.Millisecond} lo.Show(®ions) got := b.String() want := " Code Region Try #1 Try #2 Avg Latency\n" + " 0 af-south-1 Africa (Cape Town) 15.00 ms 17.00 ms 16.00 ms\n" + " 1 ap-east-1 Asia Pacific (Hong Kong) 25.00 ms 26.00 ms 25.50 ms\n" if got != want { t.Errorf("Show2 failed:\ngot =%q\nwant=%q", got, want) } } func TestCalcLatency(t *testing.T) { regions := GetRegions()[:3] regions[0].Request = &testRequest{duration: 30 * time.Millisecond} regions[1].Request = &testRequest{duration: 7 * time.Millisecond} regions[2].Request = &testRequest{duration: 15 * time.Millisecond} regionsStats := make(AWSRegions, regions.Len()) checkSort := func(origIndex, sortedIdx int) { got := regionsStats[sortedIdx].Name want := regions[origIndex].Name if got != want { t.Errorf("CalcLatency failed:\ngot=%q\nwant=%q\norig=%d\nsorted=%d", got, want, origIndex, sortedIdx) } } for i := 1; i < 4; i++ { copy(regionsStats, regions) switch i { case 1: CalcLatency(regionsStats, 1, false, false, "ec2") case 2: CalcLatency(regionsStats, 1, true, false, "ec2") default: CalcLatency(regionsStats, 1, true, true, "ec2") } checkSort(0, 2) checkSort(1, 0) checkSort(2, 1) } }