├── .gcloudignore
├── .gitignore
├── Gopkg.lock
├── Gopkg.toml
├── LICENSE
├── Makefile
├── README.md
├── app.yaml
├── bin
└── .gitkeep
├── cmd
└── fast
│ ├── download.go
│ ├── lap.go
│ ├── main.go
│ ├── recorder.go
│ └── upload.go
├── internal
├── adapter
│ ├── adapter.go
│ └── zap.go
├── config
│ └── config.go
├── logger
│ ├── logger.go
│ └── logger_test.go
└── server
│ ├── handler.go
│ └── server.go
└── main.go
/.gcloudignore:
--------------------------------------------------------------------------------
1 | # This file specifies files that are *not* uploaded to Google Cloud Platform
2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of
3 | # "#!include" directives (which insert the entries of the given .gitignore-style
4 | # file at that point).
5 | #
6 | # For more information, run:
7 | # $ gcloud topic gcloudignore
8 | #
9 | .gcloudignore
10 | # If you would like to upload your .git directory, .gitignore file or files
11 | # from your .gitignore file, remove the corresponding line
12 | # below:
13 | .git
14 | .gitignore
15 |
16 | # Binaries for programs and plugins
17 | *.exe
18 | *.exe~
19 | *.dll
20 | *.so
21 | *.dylib
22 | # Test binary, build with `go test -c`
23 | *.test
24 | # Output of the go coverage tool, specifically when used with LiteIDE
25 | *.out
26 |
27 | cmd/fast/fast
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | cmd/fast/fast
2 | vendor/
3 | bin/*
4 | !bin/.gitkeep
5 |
--------------------------------------------------------------------------------
/Gopkg.lock:
--------------------------------------------------------------------------------
1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
2 |
3 |
4 | [[projects]]
5 | digest = "1:d666a7b48e2878c5c0de95b2b3027b458692dc6a242b0cc8e4ed2c802e5e7006"
6 | name = "github.com/blendle/zapdriver"
7 | packages = ["."]
8 | pruneopts = "UT"
9 | revision = "63327ce6a5a7d5cc97dbbeab5cdc771cfc62d1e2"
10 | version = "v1.1.6"
11 |
12 | [[projects]]
13 | digest = "1:6f9339c912bbdda81302633ad7e99a28dfa5a639c864061f1929510a9a64aa74"
14 | name = "github.com/dustin/go-humanize"
15 | packages = ["."]
16 | pruneopts = "UT"
17 | revision = "9f541cc9db5d55bce703bd99987c9d5cb8eea45e"
18 | version = "v1.0.0"
19 |
20 | [[projects]]
21 | digest = "1:edbef42561faa44c19129b68d1e109fbc1647f63239250391eadc8d0e7c9f669"
22 | name = "github.com/kelseyhightower/envconfig"
23 | packages = ["."]
24 | pruneopts = "UT"
25 | revision = "f611eb38b3875cc3bd991ca91c51d06446afa14c"
26 | version = "v1.3.0"
27 |
28 | [[projects]]
29 | digest = "1:2e76a73cb51f42d63a2a1a85b3dc5731fd4faf6821b434bd0ef2c099186031d6"
30 | name = "github.com/rs/xid"
31 | packages = ["."]
32 | pruneopts = "UT"
33 | revision = "15d26544def341f036c5f8dca987a4cbe575032c"
34 | version = "v1.2.1"
35 |
36 | [[projects]]
37 | digest = "1:3c1a69cdae3501bf75e76d0d86dc6f2b0a7421bc205c0cb7b96b19eed464a34d"
38 | name = "go.uber.org/atomic"
39 | packages = ["."]
40 | pruneopts = "UT"
41 | revision = "1ea20fb1cbb1cc08cbd0d913a96dead89aa18289"
42 | version = "v1.3.2"
43 |
44 | [[projects]]
45 | digest = "1:60bf2a5e347af463c42ed31a493d817f8a72f102543060ed992754e689805d1a"
46 | name = "go.uber.org/multierr"
47 | packages = ["."]
48 | pruneopts = "UT"
49 | revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a"
50 | version = "v1.1.0"
51 |
52 | [[projects]]
53 | digest = "1:c52caf7bd44f92e54627a31b85baf06a68333a196b3d8d241480a774733dcf8b"
54 | name = "go.uber.org/zap"
55 | packages = [
56 | ".",
57 | "buffer",
58 | "internal/bufferpool",
59 | "internal/color",
60 | "internal/exit",
61 | "zapcore",
62 | ]
63 | pruneopts = "UT"
64 | revision = "ff33455a0e382e8a81d14dd7c922020b6b5e7982"
65 | version = "v1.9.1"
66 |
67 | [[projects]]
68 | branch = "master"
69 | digest = "1:b521f10a2d8fa85c04a8ef4e62f2d1e14d303599a55d64dabf9f5a02f84d35eb"
70 | name = "golang.org/x/sync"
71 | packages = ["errgroup"]
72 | pruneopts = "UT"
73 | revision = "e225da77a7e68af35c70ccbf71af2b83e6acac3c"
74 |
75 | [solve-meta]
76 | analyzer-name = "dep"
77 | analyzer-version = 1
78 | input-imports = [
79 | "github.com/blendle/zapdriver",
80 | "github.com/dustin/go-humanize",
81 | "github.com/kelseyhightower/envconfig",
82 | "github.com/rs/xid",
83 | "go.uber.org/zap",
84 | "go.uber.org/zap/zapcore",
85 | "golang.org/x/sync/errgroup",
86 | ]
87 | solver-name = "gps-cdcl"
88 | solver-version = 1
89 |
--------------------------------------------------------------------------------
/Gopkg.toml:
--------------------------------------------------------------------------------
1 | # Gopkg.toml example
2 | #
3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
4 | # for detailed Gopkg.toml documentation.
5 | #
6 | # required = ["github.com/user/thing/cmd/thing"]
7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
8 | #
9 | # [[constraint]]
10 | # name = "github.com/user/project"
11 | # version = "1.0.0"
12 | #
13 | # [[constraint]]
14 | # name = "github.com/user/project2"
15 | # branch = "dev"
16 | # source = "github.com/myfork/project2"
17 | #
18 | # [[override]]
19 | # name = "github.com/x/y"
20 | # version = "2.4.0"
21 | #
22 | # [prune]
23 | # non-go = false
24 | # go-tests = true
25 | # unused-packages = true
26 |
27 |
28 | [[constraint]]
29 | name = "github.com/kelseyhightower/envconfig"
30 | version = "1.3.0"
31 |
32 | [prune]
33 | go-tests = true
34 | unused-packages = true
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Kei Kamikawa
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build
2 | build: build/server build/cli
3 | build/server:
4 | @echo "+ $@"
5 | CGO_ENABLED=0 go build -o bin/speedtest-server \
6 | -ldflags "-w -s" \
7 | github.com/Code-Hex/fast-service
8 | build/cli:
9 | @echo "+ $@"
10 | CGO_ENABLED=0 go build -o bin/speedtest-client \
11 | -ldflags "-w -s" \
12 | github.com/Code-Hex/fast-service/cmd/fast
13 |
14 |
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # fast-service is opensource speedtest service written in Go
2 |
3 |
4 |
5 |
6 |
7 | ## Environment variables
8 |
9 | Environment variables is [here](https://github.com/Code-Hex/fast-service/blob/9e3a385f34985237c655efd9aedddbf05ef3ae45/internal/config/config.go#L12-L24)
10 |
11 | ## How to try this contents
12 |
13 | We are necessary [dep](https://github.com/golang/dep#installation) to install dependency packages.
14 |
15 | dep ensure -vendor-only
16 | make build
17 |
18 | ### How to run server
19 |
20 | ENV=development ./bin/speedtest-server
21 |
22 | ### How to run client
23 |
24 | If you want to run client on another port (default is 8000), you should rewrite [`cmd/fast/main.go:10`](https://github.com/Code-Hex/fast-service/blob/8c70fbfef8c6efcbd7e6a75e459ec8cf83dde6b5/cmd/fast/main.go#L10) before build client code.
25 |
26 | And build it like this.
27 |
28 | ./bin/speedtest-client
29 |
--------------------------------------------------------------------------------
/app.yaml:
--------------------------------------------------------------------------------
1 | runtime: go111
2 |
3 | env_variables:
4 | ENV: production
--------------------------------------------------------------------------------
/bin/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Code-Hex/fast-service/612eec2ad74cd8a670ca022ffb0d911a4b31b85f/bin/.gitkeep
--------------------------------------------------------------------------------
/cmd/fast/download.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | "golang.org/x/sync/errgroup"
8 | )
9 |
10 | var DownloadTimeout = 15 * time.Second
11 |
12 | const downloadURL = api + "/download"
13 |
14 | func DownloadTest(ctx context.Context, cb IntervalCallback) error {
15 | ctx, cancel := context.WithTimeout(ctx, DownloadTimeout)
16 | defer cancel()
17 | eg, ctx := errgroup.WithContext(ctx)
18 |
19 | r := newRecorder(time.Now(), maxConnections)
20 |
21 | go func() {
22 | for {
23 | select {
24 | case lap := <-r.Lap():
25 | cb(&lap)
26 | case <-ctx.Done():
27 | return
28 | }
29 | }
30 | }()
31 |
32 | semaphore := make(chan struct{}, maxConnections)
33 | loop:
34 | for i := 0; i < tryCount; i++ {
35 | for _, size := range payloadSizes {
36 | select {
37 | case <-ctx.Done():
38 | break loop
39 | case semaphore <- struct{}{}:
40 | time.Sleep(250 * time.Millisecond)
41 | }
42 | eg.Go(func() error {
43 | defer func() { <-semaphore }()
44 | if err := r.download(ctx, downloadURL, size); err != nil {
45 | return err
46 | }
47 | return nil
48 | })
49 | }
50 | }
51 | // waiting
52 | select {
53 | case <-ctx.Done():
54 | case semaphore <- struct{}{}:
55 | cancel()
56 | }
57 | return errorCheck(eg.Wait())
58 | }
59 |
--------------------------------------------------------------------------------
/cmd/fast/lap.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | humanize "github.com/dustin/go-humanize"
7 | )
8 |
9 | // Lap represents lap used in stopwatch.
10 | type Lap struct {
11 | Bytes int64
12 | Bps float64
13 | PrettyBps float64
14 | Units string
15 |
16 | delta float64
17 | }
18 |
19 | func newLap(byteLen int64, delta float64) Lap {
20 | var bytes float64
21 | if delta > 0 {
22 | bytes = float64(byteLen) / delta
23 | }
24 | bps := bytes * 8
25 | prettyBps, unit := humanize.ComputeSI(bps)
26 | return Lap{
27 | Bytes: byteLen,
28 | Bps: bps,
29 | PrettyBps: prettyBps,
30 | Units: unit + "bps",
31 |
32 | delta: delta,
33 | }
34 | }
35 |
36 | func (l *Lap) String() string {
37 | return fmt.Sprintf("%7.2f %s", l.PrettyBps, l.Units)
38 | }
39 |
--------------------------------------------------------------------------------
/cmd/fast/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 | "runtime"
8 | )
9 |
10 | const api = "http://localhost:8000"
11 | const tryCount = 3
12 |
13 | var loading = []rune{
14 | '⠏',
15 | '⠛',
16 | '⠹',
17 | '⠼',
18 | '⠶',
19 | '⠧',
20 | }
21 |
22 | var (
23 | maxConnections = runtime.NumCPU()
24 | payloadSizes = []int{
25 | 1562500, // 1.5625MB
26 | 6250000, // 6.25MB
27 | 12500000, // 12.5MB
28 | 26214400, // 25MB
29 | }
30 | )
31 |
32 | func main() {
33 | ctx := context.Background()
34 |
35 | var (
36 | lastDown string
37 | downBytes int64
38 | lastUp string
39 | upBytes int64
40 | )
41 | fmt.Println()
42 |
43 | var i int
44 | err := DownloadTest(ctx, func(result *Lap) error {
45 | lastDown = result.String()
46 | downBytes = result.Bytes
47 | fmt.Printf("%c%s, size: %d ↓ - %s bps, size: %d ↑\r", loading[i%len(loading)], lastDown, downBytes, "", 0)
48 | i++
49 | return nil
50 | })
51 | if err != nil {
52 | log.Fatal(err)
53 | }
54 | err = UploadTest(ctx, func(result *Lap) error {
55 | lastUp = result.String()
56 | upBytes = result.Bytes
57 | fmt.Printf("%c%s, size: %d ↓ - %s, size: %d ↑\r", loading[i%len(loading)], lastDown, downBytes, lastUp, result.Bytes)
58 | i++
59 | return nil
60 | })
61 | if err != nil {
62 | log.Fatal(err)
63 | }
64 | fmt.Printf("%s, size: %d ↓ - %s, size: %d ↑\n", lastDown, downBytes, lastUp, upBytes)
65 | }
66 |
--------------------------------------------------------------------------------
/cmd/fast/recorder.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "io/ioutil"
9 | "math/rand"
10 | "net/http"
11 | "sync/atomic"
12 | "time"
13 | )
14 |
15 | type recorder struct {
16 | byteLen int64
17 | start time.Time
18 | lapch chan Lap
19 | }
20 |
21 | func newRecorder(start time.Time, cpun int) *recorder {
22 | return &recorder{
23 | start: start,
24 | lapch: make(chan Lap, cpun),
25 | }
26 | }
27 |
28 | func (r *recorder) Lap() <-chan Lap {
29 | return r.lapch
30 | }
31 |
32 | var src = rand.NewSource(0)
33 |
34 | func (r *recorder) download(ctx context.Context, url string, size int) error {
35 | url = fmt.Sprintf("%s?size=%d", url, size)
36 | req, err := http.NewRequest("GET", url, nil)
37 | if err != nil {
38 | return err
39 | }
40 | req = req.WithContext(ctx)
41 | resp, err := http.DefaultClient.Do(req)
42 | if err != nil {
43 | return err
44 | }
45 | defer resp.Body.Close()
46 |
47 | // status check
48 | if resp.StatusCode != http.StatusOK {
49 | return errors.New(resp.Status)
50 | }
51 |
52 | // start measure
53 | proxy := r.newMeasureProxy(ctx, resp.Body)
54 |
55 | if _, err := io.Copy(ioutil.Discard, proxy); err != nil {
56 | return err
57 | }
58 | return nil
59 | }
60 |
61 | func (r *recorder) upload(ctx context.Context, url string, size int) error {
62 | // start measure
63 | proxy := r.newMeasureProxy(ctx, rand.New(rand.NewSource(0)))
64 | req, err := http.NewRequest("POST", url, proxy)
65 | if err != nil {
66 | return err
67 | }
68 | req.ContentLength = int64(size)
69 | req.Header.Set("Content-Type", "application/octet-stream")
70 | req = req.WithContext(ctx)
71 | resp, err := http.DefaultClient.Do(req)
72 | if err != nil {
73 | return err
74 | }
75 |
76 | // status check
77 | if resp.StatusCode != http.StatusOK {
78 | return errors.New(resp.Status)
79 | }
80 |
81 | return nil
82 | }
83 |
84 | type measureProxy struct {
85 | io.Reader
86 | *recorder
87 | }
88 |
89 | func (r *recorder) newMeasureProxy(ctx context.Context, reader io.Reader) io.Reader {
90 | rp := &measureProxy{
91 | Reader: reader,
92 | recorder: r,
93 | }
94 | go rp.Watch(ctx, r.lapch)
95 | return rp
96 | }
97 |
98 | func (m *measureProxy) Watch(ctx context.Context, send chan<- Lap) {
99 | t := time.NewTicker(150 * time.Millisecond)
100 | for {
101 | select {
102 | case <-t.C:
103 | byteLen := atomic.LoadInt64(&m.byteLen)
104 | delta := time.Now().Sub(m.start).Seconds()
105 | send <- newLap(byteLen, delta)
106 | case <-ctx.Done():
107 | return
108 | }
109 | }
110 | }
111 |
112 | func (m *measureProxy) Read(p []byte) (n int, err error) {
113 | n, err = m.Reader.Read(p)
114 | atomic.AddInt64(&m.byteLen, int64(n))
115 | return
116 | }
117 |
--------------------------------------------------------------------------------
/cmd/fast/upload.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "net/url"
6 | "time"
7 |
8 | "golang.org/x/sync/errgroup"
9 | )
10 |
11 | var UploadTimeout = 15 * time.Second
12 |
13 | const uploadURL = api + "/upload"
14 |
15 | type IntervalCallback func(current *Lap) error
16 |
17 | func UploadTest(ctx context.Context, cb IntervalCallback) error {
18 | ctx, cancel := context.WithTimeout(ctx, UploadTimeout)
19 | defer cancel()
20 | eg, ctx := errgroup.WithContext(ctx)
21 |
22 | r := newRecorder(time.Now(), maxConnections)
23 |
24 | go func() {
25 | for {
26 | select {
27 | case lap := <-r.Lap():
28 | cb(&lap)
29 | case <-ctx.Done():
30 | return
31 | }
32 | }
33 | }()
34 |
35 | semaphore := make(chan struct{}, maxConnections)
36 | loop:
37 | for i := 0; i < tryCount; i++ {
38 | for _, size := range payloadSizes {
39 | select {
40 | case <-ctx.Done():
41 | break loop
42 | case semaphore <- struct{}{}:
43 | time.Sleep(250 * time.Millisecond)
44 | }
45 | eg.Go(func() error {
46 | defer func() { <-semaphore }()
47 | if err := r.upload(ctx, uploadURL, size); err != nil {
48 | return err
49 | }
50 | return nil
51 | })
52 | }
53 | }
54 | // waiting
55 | select {
56 | case <-ctx.Done():
57 | case semaphore <- struct{}{}:
58 | cancel()
59 | }
60 | return errorCheck(eg.Wait())
61 | }
62 |
63 | func errorCheck(err error) error {
64 | if err == context.Canceled || err == context.DeadlineExceeded {
65 | return nil
66 | }
67 | if v, ok := err.(*url.Error); ok {
68 | err = v.Err
69 | return errorCheck(err)
70 | }
71 | return err
72 | }
73 |
--------------------------------------------------------------------------------
/internal/adapter/adapter.go:
--------------------------------------------------------------------------------
1 | package adapter
2 |
3 | // https://medium.com/@matryer/writing-middleware-in-golang-and-how-go-makes-it-so-much-fun-4375c1246e81
4 | import "net/http"
5 |
6 | // Adapter represents middleware between request and main handler
7 | type Adapter func(http.Handler) http.Handler
8 |
9 | // Adapt adapts datapters to giving handler.
10 | func Adapt(h http.Handler, adapters ...Adapter) http.Handler {
11 | // To process from left to right, iterate from the last one.
12 | for i := len(adapters) - 1; i >= 0; i-- {
13 | h = adapters[i](h)
14 | }
15 | return h
16 | }
17 |
--------------------------------------------------------------------------------
/internal/adapter/zap.go:
--------------------------------------------------------------------------------
1 | package adapter
2 |
3 | import (
4 | "net/http"
5 | "time"
6 |
7 | "github.com/Code-Hex/fast-service/internal/logger"
8 | "github.com/rs/xid"
9 | "go.uber.org/zap"
10 | )
11 |
12 | type delegator struct {
13 | http.ResponseWriter
14 | Status int
15 | }
16 |
17 | // ZapAdapter returns Adapter which adapt logging middleware.
18 | func ZapAdapter(l *zap.Logger) Adapter {
19 | return func(next http.Handler) http.Handler {
20 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
21 | start := time.Now()
22 |
23 | ctx := logger.ToContext(
24 | r.Context(),
25 | l.With(zap.Stringer("request_id", xid.New())),
26 | )
27 |
28 | d := &delegator{ResponseWriter: w}
29 | next.ServeHTTP(d, r.WithContext(ctx))
30 |
31 | logger.Info(ctx, "request",
32 | zap.String("host", r.Host),
33 | zap.String("path", r.URL.Path),
34 | zap.Int("status", d.Status),
35 | zap.Duration("duration", time.Now().Sub(start)),
36 | zap.String("method", r.Method),
37 | zap.String("user_agent", r.UserAgent()),
38 | )
39 | })
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/internal/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "github.com/kelseyhightower/envconfig"
5 | )
6 |
7 | const (
8 | envDevelopment = "development"
9 | envProduction = "production"
10 | )
11 |
12 | // Env stores configuration settings extract from enviromental variables
13 | // by using https://github.com/kelseyhightower/envconfig
14 | type Env struct {
15 | // LogLevel is INFO or DEBUG. Default is "INFO".
16 | LogLevel string `envconfig:"LOG_LEVEL" default:"INFO"`
17 |
18 | // Env is environment where application is running The value must be
19 | // "development" or "production".
20 | Env string `envconfig:"ENV" required:"true"`
21 |
22 | // Port is http serve port.
23 | Port int `envconfig:"PORT" default:"8000"`
24 | }
25 |
26 | // ReadFromEnv reads configuration from environmental variables
27 | // defined by Env struct.
28 | func ReadFromEnv() (*Env, error) {
29 | var env Env
30 | if err := envconfig.Process("", &env); err != nil {
31 | return nil, err
32 | }
33 | return &env, nil
34 | }
35 |
--------------------------------------------------------------------------------
/internal/logger/logger.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/blendle/zapdriver"
9 | "go.uber.org/zap"
10 | "go.uber.org/zap/zapcore"
11 | )
12 |
13 | type loggerKey struct{}
14 |
15 | var contextKey = &loggerKey{}
16 |
17 | // Extract takes the call-scoped Logger from context.
18 | func Extract(ctx context.Context) *zap.Logger {
19 | l, ok := ctx.Value(contextKey).(*zap.Logger)
20 | if !ok || l == nil {
21 | return NewDiscard()
22 | }
23 | return l
24 | }
25 |
26 | // ToContext adds the zap.Logger into context.
27 | func ToContext(ctx context.Context, l *zap.Logger) context.Context {
28 | return context.WithValue(ctx, contextKey, l)
29 | }
30 |
31 | // Debug logs a message at DebugLevel.
32 | func Debug(ctx context.Context, msg string, fields ...zap.Field) {
33 | Extract(ctx).Debug(msg, fields...)
34 | }
35 |
36 | // Info logs a message at InfoLevel.
37 | func Info(ctx context.Context, msg string, fields ...zap.Field) {
38 | Extract(ctx).Info(msg, fields...)
39 | }
40 |
41 | // Warn logs a message at WarnLevel.
42 | func Warn(ctx context.Context, msg string, fields ...zap.Field) {
43 | Extract(ctx).Warn(msg, fields...)
44 | }
45 |
46 | // Error logs a message at ErrorLevel.
47 | func Error(ctx context.Context, msg string, fields ...zap.Field) {
48 | Extract(ctx).Error(msg, fields...)
49 | }
50 |
51 | // New creates a new zap logger with the given log level.
52 | func New(level string) (*zap.Logger, error) {
53 | l, err := logLevel(level)
54 | if err != nil {
55 | return nil, err
56 | }
57 |
58 | config := zapdriver.NewProductionConfig()
59 | config.Level = zap.NewAtomicLevelAt(l)
60 | config.DisableStacktrace = true
61 | return config.Build()
62 | }
63 |
64 | // NewDiscard creates logger which output to ioutil.Discard.
65 | // This can be used for testing.
66 | func NewDiscard() *zap.Logger {
67 | return zap.NewNop()
68 | }
69 |
70 | func logLevel(level string) (zapcore.Level, error) {
71 | level = strings.ToUpper(level)
72 | var l zapcore.Level
73 | switch level {
74 | case "DEBUG":
75 | l = zapcore.DebugLevel
76 | case "INFO":
77 | l = zapcore.InfoLevel
78 | case "ERROR":
79 | l = zapcore.ErrorLevel
80 | default:
81 | return l, fmt.Errorf("invalid loglevel: %s", level)
82 | }
83 | return l, nil
84 | }
85 |
--------------------------------------------------------------------------------
/internal/logger/logger_test.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import (
4 | "testing"
5 |
6 | "go.uber.org/zap/zapcore"
7 | )
8 |
9 | func TestLogLevel(t *testing.T) {
10 | cases := []struct {
11 | level string
12 | success bool
13 | want zapcore.Level
14 | }{
15 | {
16 | "info",
17 | true,
18 | zapcore.InfoLevel,
19 | },
20 | {
21 | "DEBUG",
22 | true,
23 | zapcore.DebugLevel,
24 | },
25 | {
26 | "Error",
27 | true,
28 | zapcore.ErrorLevel,
29 | },
30 | {
31 | "FATAL", // not supported (debug or info is enough)
32 | false,
33 | zapcore.Level(0),
34 | },
35 | }
36 |
37 | for _, tc := range cases {
38 | got, err := logLevel(tc.level)
39 | if err != nil {
40 | if tc.success {
41 | t.Fatalf("expect to success: %s", err)
42 | }
43 | continue
44 | }
45 |
46 | if !tc.success {
47 | t.Fatal("expect to be failed")
48 | }
49 |
50 | if got != tc.want {
51 | t.Fatalf("got %v, want %v", got, tc.want)
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/internal/server/handler.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/Code-Hex/fast-service/internal/adapter"
7 | "go.uber.org/zap"
8 | )
9 |
10 | // Mux manages handlers.
11 | type Mux struct {
12 | mux *http.ServeMux
13 | adapters []adapter.Adapter
14 | }
15 |
16 | // NewMux creates new Mux http.Handler.
17 | func NewMux(logger *zap.Logger) *Mux {
18 | return &Mux{
19 | mux: http.NewServeMux(),
20 | // if set adapter like []Adapter{A(), B()}
21 | // we will access A() -> B() -> main -> B() -> A()
22 | adapters: []adapter.Adapter{
23 | adapter.ZapAdapter(logger),
24 | },
25 | }
26 | }
27 |
28 | // Handle registers the handler for the given pattern.
29 | func (m *Mux) Handle(pattern string, h http.Handler) {
30 | m.mux.Handle(pattern, adapter.Adapt(h, m.adapters...))
31 | }
32 |
--------------------------------------------------------------------------------
/internal/server/server.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "context"
5 | "net"
6 | "net/http"
7 | )
8 |
9 | // Server represents an HTTP server.
10 | type Server struct {
11 | server *http.Server
12 | }
13 |
14 | // NewServer creates new Server.
15 | func New(h http.Handler) *Server {
16 | return &Server{
17 | server: &http.Server{
18 | Handler: h,
19 | },
20 | }
21 | }
22 |
23 | // Serve starts accept requests from the given listener. If any returns error.
24 | func (s *Server) Serve(ln net.Listener) error {
25 | // ErrServerClosed is returned by the Server's Serve
26 | // after a call to Shutdown or Close, we can ignore it.
27 | if err := s.server.Serve(ln); err != nil && err != http.ErrServerClosed {
28 | return err
29 | }
30 | return nil
31 | }
32 |
33 | // Shutdown gracefully shutdown the server without interrupting any
34 | // active connections. If any returns error.
35 | func (s *Server) Shutdown(ctx context.Context) error {
36 | return s.server.Shutdown(ctx)
37 | }
38 |
39 | // ServeHTTP for represents http.Handler
40 | func (m *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
41 | m.mux.ServeHTTP(w, r)
42 | }
43 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io"
7 | "io/ioutil"
8 | "log"
9 | "math/rand"
10 | "net"
11 | "net/http"
12 | "os"
13 | "os/signal"
14 | "strconv"
15 | "syscall"
16 |
17 | "go.uber.org/zap"
18 |
19 | "github.com/Code-Hex/fast-service/internal/config"
20 | "github.com/Code-Hex/fast-service/internal/logger"
21 | "github.com/Code-Hex/fast-service/internal/server"
22 | )
23 |
24 | const maxSize = 26214400 // 25MB
25 |
26 | func main() {
27 | // Read configurations from environmental variables.
28 | env, err := config.ReadFromEnv()
29 | if err != nil {
30 | log.Fatalf("failed to read environment variables: %s", err)
31 | }
32 |
33 | // Setup new zap logger. This logger should be used for all logging in this service.
34 | // The log level can be updated via environment variables.
35 | l, err := logger.New(env.LogLevel)
36 | if err != nil {
37 | log.Fatalf("failed to prepare logger: %s", err)
38 | }
39 |
40 | if err := _main(env, l); err != nil {
41 | log.Fatalf("failed to serve: %s", err)
42 | }
43 | }
44 |
45 | func _main(env *config.Env, l *zap.Logger) error {
46 | mux := server.NewMux(l)
47 | mux.Handle("/download", downloadHandler())
48 | mux.Handle("/upload", uploadHandler())
49 |
50 | srv := server.New(mux)
51 |
52 | addr := fmt.Sprintf(":%d", env.Port)
53 | ln, err := net.Listen("tcp", addr)
54 | if err != nil {
55 | return fmt.Errorf("failed to listen port: %s", err)
56 | }
57 |
58 | ctx, cancel := context.WithCancel(context.Background())
59 | defer cancel()
60 |
61 | go func() {
62 | if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed {
63 | log.Printf("serve err: %s", err)
64 | return
65 | }
66 | }()
67 |
68 | // waiting for SIGTERM or Interrupt signal.
69 | sigCh := make(chan os.Signal, 1)
70 | signal.Notify(sigCh, syscall.SIGTERM, os.Interrupt)
71 | select {
72 | case <-sigCh:
73 | log.Printf("received SIGTERM, exiting server gracefully")
74 | case <-ctx.Done():
75 | }
76 | log.Printf("shutdown servers")
77 |
78 | if err := srv.Shutdown(ctx); err != nil {
79 | return fmt.Errorf("failed to gracefully shutdown HTTP server: %s", err)
80 | }
81 | return nil
82 | }
83 |
84 | func downloadHandler() http.HandlerFunc {
85 | src := rand.NewSource(0)
86 | return func(w http.ResponseWriter, r *http.Request) {
87 | ctx := r.Context()
88 | if r.Method != http.MethodGet {
89 | w.WriteHeader(http.StatusBadRequest)
90 | return
91 | }
92 | queries := r.URL.Query()
93 | size := queries.Get("size")
94 | max, err := strconv.Atoi(size)
95 | if err != nil {
96 | max = maxSize
97 | }
98 | if _, err := io.CopyN(w, rand.New(src), int64(max)); err != nil {
99 | logger.Error(ctx, "failed to write random file: %s", zap.Error(err))
100 | return
101 | }
102 | }
103 | }
104 |
105 | func uploadHandler() http.HandlerFunc {
106 | return func(w http.ResponseWriter, r *http.Request) {
107 | ctx := r.Context()
108 | if r.Method != http.MethodPost {
109 | w.WriteHeader(http.StatusBadRequest)
110 | return
111 | }
112 | contentType := r.Header.Get("Content-Type")
113 | if contentType != "application/octet-stream" {
114 | logger.Warn(ctx, "invalid content type", zap.String("Content-Type", contentType))
115 | w.WriteHeader(http.StatusBadRequest)
116 | return
117 | }
118 | contentLength := r.ContentLength
119 | if contentLength > maxSize {
120 | contentLength = maxSize
121 | }
122 | if _, err := io.CopyN(ioutil.Discard, r.Body, contentLength); err != nil {
123 | logger.Warn(ctx, "failed to write body", zap.Error(err))
124 | return
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------