Skip to content
This repository was archived by the owner on Dec 27, 2023. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bin/
simple-go-server
23 changes: 23 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
DOCKER_REGISTRY ?= docker.io
IMAGE_PREFIX ?= enricofoltran
SHORT_NAME ?= simple-go-server

# build options
GO ?= go
TAGS :=
LDFLAGS := -w -s
GOFLAGS :=
BINDIR := $(CURDIR)/bin

.PHONY: all
all: build

.PHONY: build
build:
GOBIN=$(BINDIR) $(GO) install $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)'

.PHONY: clean
clean:
@rm -rf $(BINDIR)

include versioning.mk
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# simple-go-server
A simple golang web server with basic logging, tracing, health check, graceful shutdown and zero dependencies.
A simple golang web server with basic logging, tracing, throttling, health check, graceful shutdown and zero dependencies.

This repository originated from **[this](https://gist.github.com/enricofoltran/10b4a980cd07cb02836f70a4ab3e72d7)** Github gist, featured on [Hacker News](https://news.ycombinator.com/item?id=16090977).

Expand Down
76 changes: 73 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,41 @@ const (
requestIDKey key = 0
)

// build info
var (
Version string = ""
GitTag string = ""
GitCommit string = ""
GitTreeState string = ""
)

// flags
var (
listenAddr string
healthy int32
rps int
burst int
timeout time.Duration
)

var healthy int32

func main() {
flag.StringVar(&listenAddr, "listen-addr", ":5000", "server listen address")
flag.IntVar(&rps, "rps", 425, "requests per second throttling limit")
flag.IntVar(&burst, "burst", 10, "concurrently handled requests before queueing")
flag.DurationVar(&timeout, "timeout", 75*time.Millisecond, "time after which quequed requests are dropped")
flag.Parse()

logger := log.New(os.Stdout, "http: ", log.LstdFlags)

logger.Println("Simple go server")
logger.Println("Version:", Version)
logger.Println("GitTag:", GitTag)
logger.Println("GitCommit:", GitCommit)
logger.Println("GitTreeState:", GitTreeState)

logger.Println("Server is starting...")
logger.Printf("Settings are: rps=%v burst=%v timeout=%v", rps, burst, timeout)

router := http.NewServeMux()
router.Handle("/", index())
Expand All @@ -39,8 +63,12 @@ func main() {
}

server := &http.Server{
Addr: listenAddr,
Handler: tracing(nextRequestID)(logging(logger)(router)),
Addr: listenAddr,
Handler: throttling(rps, burst, timeout)(
tracing(nextRequestID)(
logging(logger)(router),
),
),
ErrorLog: logger,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
Expand Down Expand Up @@ -82,6 +110,10 @@ func index() http.Handler {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
if r.Method != http.MethodGet {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(http.StatusOK)
Expand Down Expand Up @@ -127,3 +159,41 @@ func tracing(nextRequestID func() string) func(http.Handler) http.Handler {
})
}
}

// adapted from https://rodaine.com/2017/05/x-files-time-rate-golang/
func throttling(rps, burst int, timeout time.Duration) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
limiter, _ := throttler(rps, burst)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
timer := time.NewTimer(timeout)
select {
case <-limiter:
timer.Stop()
case <-timer.C:
http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}

func throttler(rps, burst int) (c chan time.Time, cancel func()) {
c = make(chan time.Time, burst)
for i := 0; i < burst; i++ {
c <- time.Now()
}

tick := time.NewTicker(time.Second / time.Duration(rps))
go func() {
for t := range tick.C {
select {
case c <- t:
default:
}
}
close(c)
}()

return c, tick.Stop
}
17 changes: 17 additions & 0 deletions rootfs/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM alpine:3.8 as builder

RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/*

RUN adduser -D -g '' app

# ---
FROM scratch

COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /etc/passwd /etc/passwd

COPY simple-go-server /simple-go-server

USER app

ENTRYPOINT ["/simple-go-server"]
64 changes: 64 additions & 0 deletions versioning.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
GIT_COMMIT = $(shell git rev-parse HEAD)
GIT_SHA = $(shell git rev-parse --short HEAD)
GIT_TAG = $(shell git describe --tags --abbrev=0 --exact-match 2>/dev/null)
GIT_DIRTY = $(shell test -n "`git status --porcelain`" && echo "dirty" || echo "clean")

ifdef VERSION
DOCKER_VERSION = $(VERSION)
BINARY_VERSION = $(VERSION)
endif

MUTABLE_VERSION := canary
DOCKER_VERSION ?= git-${GIT_SHA}
BINARY_VERSION ?= ${GIT_TAG}

# Only set Version if building a tag or VERSION is set
ifneq ($(BINARY_VERSION),)
LDFLAGS += -X main.Version=${BINARY_VERSION}
endif

LDFLAGS += -X main.GitTag=${GIT_TAG}
LDFLAGS += -X main.GitCommit=${GIT_SHA}
LDFLAGS += -X main.GitTreeState=${GIT_DIRTY}

IMAGE := ${DOCKER_REGISTRY}/${IMAGE_PREFIX}/${SHORT_NAME}:${DOCKER_VERSION}
MUTABLE_IMAGE := ${DOCKER_REGISTRY}/${IMAGE_PREFIX}/${SHORT_NAME}:${MUTABLE_VERSION}

info:
@echo "Version: ${VERSION}"
@echo "Git Tag: ${GIT_TAG}"
@echo "Git Commit: ${GIT_COMMIT}"
@echo "Git Tree State: ${GIT_DIRTY}"
@echo "Docker Version: ${DOCKER_VERSION}"
@echo "Registry: ${DOCKER_REGISTRY}"
@echo "Immutable Image: ${IMAGE}"
@echo "Mutable Image: ${MUTABLE_IMAGE}"

.PHONY: check-docker
check-docker:
@if [ -z $$(which docker) ]; then \
echo "Missing \`docker\` client which is required for development"; \
exit 2; \
fi

.PHONY: docker-binary
docker-binary: BINDIR = $(CURDIR)/rootfs
docker-binary: GOFLAGS += -a -installsuffix cgo
docker-binary:
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 $(GO) build -o $(BINDIR)/$(SHORT_NAME) $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)'

.PHONY: docker-build
docker-build: check-docker docker-binary
docker build --rm -t ${IMAGE} rootfs
docker tag ${IMAGE} ${MUTABLE_IMAGE}

.PHONY: docker-push
docker-push: docker-mutable-push docker-immutable-push

.PHONY: docker-immutable-push
docker-immutable-push:
docker push ${IMAGE}

.PHONY: docker-mutable-push
docker-mutable-push:
docker push ${MUTABLE_IMAGE}