mirror of
https://github.com/tarampampam/error-pages.git
synced 2024-08-30 18:22:40 +00:00
chore: Better CLI (#163)
This commit is contained in:
parent
315c7660d1
commit
252618a975
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@ -129,7 +129,7 @@ jobs: # Docs: <https://git.io/JvxXE>
|
||||
|
||||
- name: Try to execute
|
||||
if: matrix.os == 'linux'
|
||||
run: ./error-pages version && ./error-pages -h
|
||||
run: ./error-pages --version && ./error-pages -h
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@ -155,7 +155,7 @@ jobs: # Docs: <https://git.io/JvxXE>
|
||||
run: mv ./error-pages ./../error-pages && chmod +x ./../error-pages
|
||||
|
||||
- name: Run generator
|
||||
run: ./error-pages build ./out --verbose --index
|
||||
run: ./error-pages --verbose build --index ./out
|
||||
|
||||
- name: Test files creation
|
||||
run: |
|
||||
|
10
CHANGELOG.md
10
CHANGELOG.md
@ -4,6 +4,16 @@ All notable changes to this package will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog][keepachangelog] and this project adheres to [Semantic Versioning][semver].
|
||||
|
||||
## UNRELEASED
|
||||
|
||||
### Changed
|
||||
|
||||
- `version` subcommand replaced by `--version` flag [#163]
|
||||
- `--config-file` flag is not global anymore (use `error-pages (serve|build) --config-file ...` instead of `error-pages --config-file ... (serve|build) ...`) [#163]
|
||||
- Flags `--verbose`, `--debug` and `--log-json` are deprecated, use `--log-level` and `--log-format` instead [#163]
|
||||
|
||||
[#163]:https://github.com/tarampampam/error-pages/pull/163
|
||||
|
||||
## v2.19.0
|
||||
|
||||
### Changed
|
||||
|
@ -16,7 +16,7 @@ ENV LDFLAGS="-s -w -X github.com/tarampampam/error-pages/internal/version.versio
|
||||
RUN set -x \
|
||||
&& go version \
|
||||
&& CGO_ENABLED=0 go build -trimpath -ldflags "$LDFLAGS" -o ./error-pages ./cmd/error-pages/ \
|
||||
&& ./error-pages version \
|
||||
&& ./error-pages --version \
|
||||
&& ./error-pages -h
|
||||
|
||||
WORKDIR /tmp/rootfs
|
||||
@ -38,7 +38,7 @@ WORKDIR /tmp/rootfs/opt
|
||||
|
||||
# generate static error pages (for usage inside another docker images, for example)
|
||||
RUN set -x \
|
||||
&& ./../bin/error-pages --config-file ./error-pages.yml build ./html --verbose --index \
|
||||
&& ./../bin/error-pages --verbose build --config-file ./error-pages.yml --index ./html \
|
||||
&& ls -l ./html
|
||||
|
||||
# use empty filesystem
|
||||
@ -72,8 +72,8 @@ ENV LISTEN_PORT="8080" \
|
||||
DISABLE_L10N="false"
|
||||
|
||||
# Docs: <https://docs.docker.com/engine/reference/builder/#healthcheck>
|
||||
HEALTHCHECK --interval=7s --timeout=2s CMD ["/bin/error-pages", "healthcheck", "--log-json"]
|
||||
HEALTHCHECK --interval=7s --timeout=2s CMD ["/bin/error-pages", "--log-json", "healthcheck"]
|
||||
|
||||
ENTRYPOINT ["/bin/error-pages"]
|
||||
|
||||
CMD ["serve", "--log-json"]
|
||||
CMD ["--log-json", "serve"]
|
||||
|
@ -1,13 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
crypto "crypto/rand"
|
||||
"encoding/binary"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"go.uber.org/automaxprocs/maxprocs"
|
||||
|
||||
"github.com/tarampampam/error-pages/internal/cli"
|
||||
)
|
||||
|
||||
// set GOMAXPROCS to match Linux container CPU quota.
|
||||
var _, _ = maxprocs.Set(maxprocs.Min(1), maxprocs.Logger(func(_ string, _ ...any) {}))
|
||||
|
||||
// exitFn is a function for application exiting.
|
||||
var exitFn = os.Exit //nolint:gochecknoglobals
|
||||
|
||||
@ -17,9 +26,16 @@ func main() { exitFn(run()) }
|
||||
// run this CLI application.
|
||||
// Exit codes documentation: <https://tldp.org/LDP/abs/html/exitcodes.html>
|
||||
func run() int {
|
||||
cmd := cli.NewCommand(filepath.Base(os.Args[0]))
|
||||
var b [8]byte
|
||||
|
||||
if err := cmd.Execute(); err != nil {
|
||||
// seed random number generator
|
||||
if _, err := crypto.Read(b[:]); err == nil {
|
||||
rand.Seed(int64(binary.LittleEndian.Uint64(b[:]))) // https://stackoverflow.com/a/54491783/2252921
|
||||
} else {
|
||||
rand.Seed(time.Now().UnixNano()) // fallback
|
||||
}
|
||||
|
||||
if err := (cli.NewApp(filepath.Base(os.Args[0]))).Run(os.Args); err != nil {
|
||||
_, _ = color.New(color.FgHiRed, color.Bold).Fprintln(os.Stderr, err.Error())
|
||||
|
||||
return 1
|
||||
|
@ -6,36 +6,16 @@ import (
|
||||
|
||||
"github.com/kami-zh/go-capturer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_Main(t *testing.T) {
|
||||
func Test_MainHelp(t *testing.T) {
|
||||
os.Args = []string{"", "--help"}
|
||||
exitFn = func(code int) { assert.Equal(t, 0, code) }
|
||||
exitFn = func(code int) { require.Equal(t, 0, code) }
|
||||
|
||||
output := capturer.CaptureStdout(main)
|
||||
|
||||
assert.Contains(t, output, "Usage:")
|
||||
assert.Contains(t, output, "Available Commands:")
|
||||
assert.Contains(t, output, "Flags:")
|
||||
}
|
||||
|
||||
func Test_MainWithoutCommands(t *testing.T) {
|
||||
os.Args = []string{""}
|
||||
exitFn = func(code int) { assert.Equal(t, 0, code) }
|
||||
|
||||
output := capturer.CaptureStdout(main)
|
||||
|
||||
assert.Contains(t, output, "Usage:")
|
||||
assert.Contains(t, output, "Available Commands:")
|
||||
assert.Contains(t, output, "Flags:")
|
||||
}
|
||||
|
||||
func Test_MainUnknownSubcommand(t *testing.T) {
|
||||
os.Args = []string{"", "foobar"}
|
||||
exitFn = func(code int) { assert.Equal(t, 1, code) }
|
||||
|
||||
output := capturer.CaptureStderr(main)
|
||||
|
||||
assert.Contains(t, output, "unknown command")
|
||||
assert.Contains(t, output, "foobar")
|
||||
assert.Contains(t, output, "USAGE:")
|
||||
assert.Contains(t, output, "COMMANDS:")
|
||||
assert.Contains(t, output, "GLOBAL OPTIONS:")
|
||||
}
|
||||
|
9
go.mod
9
go.mod
@ -10,10 +10,11 @@ require (
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.14.0
|
||||
github.com/prometheus/client_model v0.3.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/urfave/cli/v2 v2.24.1
|
||||
github.com/valyala/fasthttp v1.43.0
|
||||
go.uber.org/automaxprocs v1.5.1
|
||||
go.uber.org/goleak v1.1.11
|
||||
go.uber.org/zap v1.24.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
@ -22,9 +23,9 @@ require (
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/klauspost/compress v1.15.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
@ -32,8 +33,10 @@ require (
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
|
||||
|
26
go.sum
26
go.sum
@ -56,6 +56,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
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=
|
||||
@ -138,8 +139,6 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
@ -183,6 +182,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
@ -210,16 +210,13 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
|
||||
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d h1:Q+gqLBOPkFGHyCJxXMRqtUgUbTjI8/Ze8vu8GGyNFwo=
|
||||
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
||||
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
@ -227,19 +224,25 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/urfave/cli/v2 v2.24.1 h1:/QYYr7g0EhwXEML8jO+8OYt5trPnLHS0p3mrgExJ5NU=
|
||||
github.com/urfave/cli/v2 v2.24.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.42.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
|
||||
github.com/valyala/fasthttp v1.43.0 h1:Gy4sb32C98fbzVWZlTM1oTMdLWGyvxR03VhM6cBIU4g=
|
||||
github.com/valyala/fasthttp v1.43.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
@ -247,7 +250,10 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/automaxprocs v1.5.1 h1:e1YG66Lrk73dn4qhg8WFSvhF0JuFQF0ERIp4rpuV8Qk=
|
||||
go.uber.org/automaxprocs v1.5.1/go.mod h1:BF4eumQw0P9GtnuxxovUd06vwm1o18oMzFtK66vU6XU=
|
||||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||
@ -280,6 +286,7 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
@ -289,6 +296,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -317,6 +325,7 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
@ -338,6 +347,7 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -372,7 +382,9 @@ golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -433,6 +445,8 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
103
internal/cli/app.go
Normal file
103
internal/cli/app.go
Normal file
@ -0,0 +1,103 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/tarampampam/error-pages/internal/checkers"
|
||||
"github.com/tarampampam/error-pages/internal/cli/build"
|
||||
"github.com/tarampampam/error-pages/internal/cli/healthcheck"
|
||||
"github.com/tarampampam/error-pages/internal/cli/serve"
|
||||
"github.com/tarampampam/error-pages/internal/env"
|
||||
"github.com/tarampampam/error-pages/internal/logger"
|
||||
"github.com/tarampampam/error-pages/internal/version"
|
||||
)
|
||||
|
||||
// NewApp creates new console application.
|
||||
func NewApp(appName string) *cli.App { //nolint:funlen
|
||||
const (
|
||||
logLevelFlagName = "log-level"
|
||||
logFormatFlagName = "log-format"
|
||||
verboseFlagName = "verbose"
|
||||
debugFlagName = "debug"
|
||||
logJSONFlagName = "log-json"
|
||||
|
||||
defaultLogLevel = logger.InfoLevel
|
||||
defaultLogFormat = logger.ConsoleFormat
|
||||
)
|
||||
|
||||
// create "default" logger (will be overwritten later with customized)
|
||||
var log, _ = logger.New(defaultLogLevel, defaultLogFormat) // error will never occurs
|
||||
|
||||
return &cli.App{
|
||||
Usage: appName,
|
||||
Before: func(c *cli.Context) (err error) {
|
||||
_ = log.Sync() // sync previous logger instance
|
||||
|
||||
var logLevel, logFormat = defaultLogLevel, defaultLogFormat //nolint:ineffassign
|
||||
|
||||
if c.Bool(verboseFlagName) || c.Bool(debugFlagName) {
|
||||
logLevel = logger.DebugLevel
|
||||
} else {
|
||||
// parse logging level
|
||||
if logLevel, err = logger.ParseLevel(c.String(logLevelFlagName)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.Bool(logJSONFlagName) {
|
||||
logFormat = logger.JSONFormat
|
||||
} else {
|
||||
// parse logging format
|
||||
if logFormat, err = logger.ParseFormat(c.String(logFormatFlagName)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
configured, err := logger.New(logLevel, logFormat) // create new logger instance
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*log = *configured // replace "default" logger with customized
|
||||
|
||||
return nil
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
healthcheck.NewCommand(checkers.NewHealthChecker(context.TODO())),
|
||||
build.NewCommand(log),
|
||||
serve.NewCommand(log),
|
||||
},
|
||||
Version: fmt.Sprintf("%s (%s)", version.Version(), runtime.Version()),
|
||||
Flags: []cli.Flag{ // global flags
|
||||
&cli.BoolFlag{ // kept for backward compatibility
|
||||
Name: verboseFlagName,
|
||||
Usage: "verbose output (DEPRECATED FLAG)",
|
||||
},
|
||||
&cli.BoolFlag{ // kept for backward compatibility
|
||||
Name: debugFlagName,
|
||||
Usage: "debug output (DEPRECATED FLAG)",
|
||||
},
|
||||
&cli.BoolFlag{ // kept for backward compatibility
|
||||
Name: logJSONFlagName,
|
||||
Usage: "logs in JSON format (DEPRECATED FLAG)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: logLevelFlagName,
|
||||
Value: defaultLogLevel.String(),
|
||||
Usage: "logging level (`" + strings.Join(logger.LevelStrings(), "/") + "`)",
|
||||
EnvVars: []string{env.LogLevel.String()},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: logFormatFlagName,
|
||||
Value: defaultLogFormat.String(),
|
||||
Usage: "logging format (`" + strings.Join(logger.FormatStrings(), "/") + "`)",
|
||||
EnvVars: []string{env.LogFormat.String()},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
19
internal/cli/app_test.go
Normal file
19
internal/cli/app_test.go
Normal file
@ -0,0 +1,19 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/tarampampam/error-pages/internal/cli"
|
||||
)
|
||||
|
||||
func TestNewCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := cli.NewApp("app")
|
||||
|
||||
assert.NotEmpty(t, app.Flags)
|
||||
|
||||
assert.NoError(t, app.Run([]string{"", "--log-level", "debug", "--log-format", "json"}))
|
||||
}
|
@ -5,60 +5,59 @@ import (
|
||||
"path"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/tarampampam/error-pages/internal/cli/shared"
|
||||
"github.com/tarampampam/error-pages/internal/config"
|
||||
"github.com/tarampampam/error-pages/internal/tpl"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type command struct {
|
||||
c *cli.Command
|
||||
}
|
||||
|
||||
// NewCommand creates `build` command.
|
||||
func NewCommand(log *zap.Logger, configFile *string) *cobra.Command {
|
||||
var (
|
||||
generateIndex bool
|
||||
disableL10n bool
|
||||
cfg *config.Config
|
||||
func NewCommand(log *zap.Logger) *cli.Command {
|
||||
var cmd = command{}
|
||||
|
||||
const (
|
||||
generateIndexFlagName = "index"
|
||||
disableL10nFlagName = "disable-l10n"
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "build <output-directory>",
|
||||
Aliases: []string{"b"},
|
||||
Short: "Build the error pages",
|
||||
Args: cobra.ExactArgs(1),
|
||||
PreRunE: func(*cobra.Command, []string) (err error) {
|
||||
if configFile == nil {
|
||||
return errors.New("path to the config file is required for this command")
|
||||
cmd.c = &cli.Command{
|
||||
Name: "build",
|
||||
Aliases: []string{"b"},
|
||||
Usage: "build <output-directory>",
|
||||
Description: "Build the error pages",
|
||||
Action: func(c *cli.Context) error {
|
||||
cfg, cfgErr := config.FromYamlFile(c.String(shared.ConfigFileFlag.Name))
|
||||
if cfgErr != nil {
|
||||
return cfgErr
|
||||
}
|
||||
|
||||
if cfg, err = config.FromYamlFile(*configFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
if len(args) != 1 {
|
||||
if c.Args().Len() != 1 {
|
||||
return errors.New("wrong arguments count")
|
||||
}
|
||||
|
||||
return run(log, cfg, args[0], generateIndex, disableL10n)
|
||||
return cmd.Run(log, cfg, c.Args().First(), c.Bool(generateIndexFlagName), c.Bool(disableL10nFlagName))
|
||||
},
|
||||
Flags: []cli.Flag{ // global flags
|
||||
&cli.BoolFlag{
|
||||
Name: generateIndexFlagName,
|
||||
Aliases: []string{"i"},
|
||||
Usage: "generate index page",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: disableL10nFlagName,
|
||||
Usage: "disable error pages localization",
|
||||
},
|
||||
shared.ConfigFileFlag,
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(
|
||||
&generateIndex,
|
||||
"index", "i",
|
||||
false,
|
||||
"generate index page",
|
||||
)
|
||||
|
||||
cmd.Flags().BoolVarP(
|
||||
&disableL10n,
|
||||
"disable-l10n", "",
|
||||
false,
|
||||
"disable error pages localization",
|
||||
)
|
||||
|
||||
return cmd
|
||||
return cmd.c
|
||||
}
|
||||
|
||||
const (
|
||||
@ -68,14 +67,14 @@ const (
|
||||
outDirPerm = os.FileMode(0775)
|
||||
)
|
||||
|
||||
func run(log *zap.Logger, cfg *config.Config, outDirectoryPath string, generateIndex, disableL10n bool) error { //nolint:funlen,lll
|
||||
func (cmd *command) Run(log *zap.Logger, cfg *config.Config, outDirectoryPath string, generateIndex, disableL10n bool) error { //nolint:funlen,lll
|
||||
if len(cfg.Templates) == 0 {
|
||||
return errors.New("no loaded templates")
|
||||
}
|
||||
|
||||
log.Info("output directory preparing", zap.String("path", outDirectoryPath))
|
||||
|
||||
if err := createDirectory(outDirectoryPath, outDirPerm); err != nil {
|
||||
if err := cmd.createDirectory(outDirectoryPath, outDirPerm); err != nil {
|
||||
return errors.Wrap(err, "cannot prepare output directory")
|
||||
}
|
||||
|
||||
@ -86,7 +85,7 @@ func run(log *zap.Logger, cfg *config.Config, outDirectoryPath string, generateI
|
||||
log.Debug("template processing", zap.String("name", template.Name()))
|
||||
|
||||
for _, page := range cfg.Pages {
|
||||
if err := createDirectory(path.Join(outDirectoryPath, template.Name()), outDirPerm); err != nil {
|
||||
if err := cmd.createDirectory(path.Join(outDirectoryPath, template.Name()), outDirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -138,7 +137,7 @@ func run(log *zap.Logger, cfg *config.Config, outDirectoryPath string, generateI
|
||||
return nil
|
||||
}
|
||||
|
||||
func createDirectory(path string, perm os.FileMode) error {
|
||||
func (cmd *command) createDirectory(path string, perm os.FileMode) error {
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
|
@ -1,7 +1,26 @@
|
||||
package build_test
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
func TestNothing(t *testing.T) {
|
||||
t.Skip("tests for this package have not been implemented yet")
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.uber.org/goleak"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/tarampampam/error-pages/internal/cli/build"
|
||||
)
|
||||
|
||||
func TestNewCommand(t *testing.T) {
|
||||
defer goleak.VerifyNone(t)
|
||||
|
||||
cmd := build.NewCommand(zap.NewNop())
|
||||
|
||||
assert.NotEmpty(t, cmd.Flags)
|
||||
|
||||
assert.Error(t, cmd.Run(
|
||||
cli.NewContext(cli.NewApp(), &flag.FlagSet{}, nil),
|
||||
"",
|
||||
), "should fail because of missing external services")
|
||||
}
|
||||
|
@ -2,57 +2,35 @@
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"errors"
|
||||
"math"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/tarampampam/error-pages/internal/env"
|
||||
"github.com/tarampampam/error-pages/internal/cli/shared"
|
||||
)
|
||||
|
||||
type checker interface {
|
||||
Check(port uint16) error
|
||||
}
|
||||
|
||||
const portFlagName = "port"
|
||||
|
||||
// NewCommand creates `healthcheck` command.
|
||||
func NewCommand(checker checker) *cobra.Command {
|
||||
var port uint16
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "healthcheck",
|
||||
func NewCommand(checker checker) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "healthcheck",
|
||||
Aliases: []string{"chk", "health", "check"},
|
||||
Short: "Health checker for the HTTP server. Use case - docker healthcheck",
|
||||
PreRunE: func(c *cobra.Command, _ []string) (lastErr error) {
|
||||
c.Flags().VisitAll(func(flag *pflag.Flag) {
|
||||
// flag was NOT defined using CLI (flags should have maximal priority)
|
||||
if !flag.Changed && flag.Name == portFlagName {
|
||||
if envPort, exists := env.ListenPort.Lookup(); exists && envPort != "" {
|
||||
if p, err := strconv.ParseUint(envPort, 10, 16); err == nil {
|
||||
port = uint16(p)
|
||||
} else {
|
||||
lastErr = fmt.Errorf("wrong TCP port environment variable [%s] value", envPort)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Usage: "Health checker for the HTTP server. Use case - docker healthcheck",
|
||||
Action: func(c *cli.Context) error {
|
||||
var port = c.Uint(shared.ListenPortFlag.Name)
|
||||
|
||||
return lastErr
|
||||
if port <= 0 || port > math.MaxUint16 {
|
||||
return errors.New("port value out of range")
|
||||
}
|
||||
|
||||
return checker.Check(uint16(port))
|
||||
},
|
||||
RunE: func(*cobra.Command, []string) error {
|
||||
return checker.Check(port)
|
||||
Flags: []cli.Flag{
|
||||
shared.ListenPortFlag,
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Uint16VarP(
|
||||
&port,
|
||||
portFlagName,
|
||||
"p",
|
||||
8080, //nolint:gomnd // must be same as default serve `--port` flag value
|
||||
fmt.Sprintf("TCP port number [$%s]", env.ListenPort),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
@ -2,12 +2,12 @@ package healthcheck_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/kami-zh/go-capturer"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/tarampampam/error-pages/internal/cli/healthcheck"
|
||||
)
|
||||
|
||||
@ -18,77 +18,30 @@ func (c *fakeChecker) Check(port uint16) error { return c.err }
|
||||
func TestProperties(t *testing.T) {
|
||||
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
|
||||
|
||||
assert.Equal(t, "healthcheck", cmd.Use)
|
||||
assert.Equal(t, "healthcheck", cmd.Name)
|
||||
assert.ElementsMatch(t, []string{"chk", "health", "check"}, cmd.Aliases)
|
||||
assert.NotNil(t, cmd.RunE)
|
||||
assert.NotNil(t, cmd.Action)
|
||||
}
|
||||
|
||||
func TestCommandRun(t *testing.T) {
|
||||
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
|
||||
cmd.SetArgs([]string{})
|
||||
|
||||
output := capturer.CaptureOutput(func() {
|
||||
assert.NoError(t, cmd.Execute())
|
||||
})
|
||||
|
||||
assert.Empty(t, output)
|
||||
assert.NoError(t, cmd.Run(cli.NewContext(cli.NewApp(), &flag.FlagSet{}, nil)))
|
||||
}
|
||||
|
||||
func TestCommandRunFailed(t *testing.T) {
|
||||
cmd := healthcheck.NewCommand(&fakeChecker{err: errors.New("foo err")})
|
||||
cmd.SetArgs([]string{})
|
||||
|
||||
output := capturer.CaptureStderr(func() {
|
||||
assert.Error(t, cmd.Execute())
|
||||
})
|
||||
|
||||
assert.Contains(t, output, "foo err")
|
||||
assert.ErrorContains(t, cmd.Run(cli.NewContext(cli.NewApp(), &flag.FlagSet{}, nil)), "foo err")
|
||||
}
|
||||
|
||||
func TestPortFlagWrongArgument(t *testing.T) {
|
||||
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
|
||||
cmd.SetArgs([]string{"-p", "65536"}) // 65535 is max
|
||||
|
||||
var executed bool
|
||||
err := cmd.Run(
|
||||
cli.NewContext(cli.NewApp(), &flag.FlagSet{}, nil),
|
||||
"", "-p", "65536",
|
||||
)
|
||||
|
||||
cmd.RunE = func(*cobra.Command, []string) error {
|
||||
executed = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
output := capturer.CaptureStderr(func() {
|
||||
assert.Error(t, cmd.Execute())
|
||||
})
|
||||
|
||||
assert.Contains(t, output, "invalid argument")
|
||||
assert.Contains(t, output, "65536")
|
||||
assert.Contains(t, output, "value out of range")
|
||||
assert.False(t, executed)
|
||||
}
|
||||
|
||||
func TestPortFlagWrongEnvValue(t *testing.T) {
|
||||
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
|
||||
cmd.SetArgs([]string{})
|
||||
|
||||
assert.NoError(t, os.Setenv("LISTEN_PORT", "65536")) // 65535 is max
|
||||
|
||||
defer func() { assert.NoError(t, os.Unsetenv("LISTEN_PORT")) }()
|
||||
|
||||
var executed bool
|
||||
|
||||
cmd.RunE = func(*cobra.Command, []string) error {
|
||||
executed = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
output := capturer.CaptureStderr(func() {
|
||||
assert.Error(t, cmd.Execute())
|
||||
})
|
||||
|
||||
assert.Contains(t, output, "wrong TCP port")
|
||||
assert.Contains(t, output, "environment variable")
|
||||
assert.Contains(t, output, "65536")
|
||||
assert.False(t, executed)
|
||||
assert.ErrorContains(t, err, "port value out of range")
|
||||
}
|
||||
|
@ -1,93 +0,0 @@
|
||||
// Package cli contains CLI command handlers.
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/tarampampam/error-pages/internal/checkers"
|
||||
buildCmd "github.com/tarampampam/error-pages/internal/cli/build"
|
||||
healthcheckCmd "github.com/tarampampam/error-pages/internal/cli/healthcheck"
|
||||
serveCmd "github.com/tarampampam/error-pages/internal/cli/serve"
|
||||
versionCmd "github.com/tarampampam/error-pages/internal/cli/version"
|
||||
"github.com/tarampampam/error-pages/internal/env"
|
||||
"github.com/tarampampam/error-pages/internal/logger"
|
||||
"github.com/tarampampam/error-pages/internal/version"
|
||||
)
|
||||
|
||||
const configFileFlagName = "config-file"
|
||||
|
||||
// NewCommand creates root command.
|
||||
func NewCommand(appName string) *cobra.Command { //nolint:funlen
|
||||
var (
|
||||
configFile string
|
||||
verbose bool
|
||||
debug bool
|
||||
logJSON bool
|
||||
)
|
||||
|
||||
ctx := context.Background() // main CLI context
|
||||
|
||||
// create "default" logger (will be overwritten later with customized)
|
||||
log, err := logger.New(false, false, false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: appName,
|
||||
PersistentPreRunE: func(c *cobra.Command, _ []string) error {
|
||||
_ = log.Sync() // sync previous logger instance
|
||||
|
||||
customizedLog, e := logger.New(verbose, debug, logJSON)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
*log = *customizedLog // override "default" logger with customized
|
||||
|
||||
c.Flags().VisitAll(func(flag *pflag.Flag) {
|
||||
// flag was NOT defined using CLI (flags should have maximal priority)
|
||||
if !flag.Changed && flag.Name == configFileFlagName {
|
||||
if envConfigFile, exists := env.ConfigFilePath.Lookup(); exists && envConfigFile != "" {
|
||||
configFile = envConfigFile
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
},
|
||||
PersistentPostRun: func(*cobra.Command, []string) {
|
||||
// error ignoring reasons:
|
||||
// - <https://github.com/uber-go/zap/issues/772>
|
||||
// - <https://github.com/uber-go/zap/issues/328>
|
||||
_ = log.Sync()
|
||||
},
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
CompletionOptions: cobra.CompletionOptions{
|
||||
DisableDefaultCmd: true,
|
||||
},
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
|
||||
cmd.PersistentFlags().BoolVarP(&debug, "debug", "", false, "debug output")
|
||||
cmd.PersistentFlags().BoolVarP(&logJSON, "log-json", "", false, "logs in JSON format")
|
||||
cmd.PersistentFlags().StringVarP(
|
||||
&configFile,
|
||||
configFileFlagName, "c",
|
||||
"./error-pages.yml",
|
||||
fmt.Sprintf("path to the config file [$%s]", env.ConfigFilePath),
|
||||
)
|
||||
|
||||
cmd.AddCommand(
|
||||
versionCmd.NewCommand(version.Version()),
|
||||
healthcheckCmd.NewCommand(checkers.NewHealthChecker(ctx)),
|
||||
buildCmd.NewCommand(log, &configFile),
|
||||
serveCmd.NewCommand(ctx, log, &configFile),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/cli"
|
||||
)
|
||||
|
||||
func TestSubcommands(t *testing.T) {
|
||||
cmd := cli.NewCommand("unit test")
|
||||
|
||||
cases := []struct {
|
||||
giveName string
|
||||
}{
|
||||
{giveName: "build"},
|
||||
{giveName: "version"},
|
||||
{giveName: "healthcheck"},
|
||||
{giveName: "serve"},
|
||||
}
|
||||
|
||||
// get all existing subcommands and put into the map
|
||||
subcommands := make(map[string]*cobra.Command)
|
||||
for _, sub := range cmd.Commands() {
|
||||
subcommands[sub.Name()] = sub
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
tt := tt
|
||||
t.Run(tt.giveName, func(t *testing.T) {
|
||||
if _, exists := subcommands[tt.giveName]; !exists {
|
||||
assert.Failf(t, "command not found", "command [%s] was not found", tt.giveName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlags(t *testing.T) {
|
||||
cmd := cli.NewCommand("unit test")
|
||||
|
||||
cases := []struct {
|
||||
giveName string
|
||||
wantShorthand string
|
||||
wantDefault string
|
||||
}{
|
||||
{giveName: "verbose", wantShorthand: "v", wantDefault: "false"},
|
||||
{giveName: "debug", wantShorthand: "", wantDefault: "false"},
|
||||
{giveName: "log-json", wantShorthand: "", wantDefault: "false"},
|
||||
{giveName: "config-file", wantShorthand: "c", wantDefault: "./error-pages.yml"},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
tt := tt
|
||||
t.Run(tt.giveName, func(t *testing.T) {
|
||||
flag := cmd.Flag(tt.giveName)
|
||||
|
||||
if flag == nil {
|
||||
assert.Failf(t, "flag not found", "flag [%s] was not found", tt.giveName)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.wantShorthand, flag.Shorthand)
|
||||
assert.Equal(t, tt.wantDefault, flag.DefValue)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuting(t *testing.T) {
|
||||
cmd := cli.NewCommand("unit test")
|
||||
cmd.SetArgs([]string{})
|
||||
|
||||
var executed bool
|
||||
|
||||
if cmd.Run == nil { // override "Run" property for test (if it was not set)
|
||||
cmd.Run = func(cmd *cobra.Command, args []string) {
|
||||
executed = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.NoError(t, cmd.Execute())
|
||||
assert.True(t, executed)
|
||||
}
|
@ -3,53 +3,164 @@ package serve
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/tarampampam/error-pages/internal/breaker"
|
||||
"github.com/tarampampam/error-pages/internal/config"
|
||||
appHttp "github.com/tarampampam/error-pages/internal/http"
|
||||
"github.com/tarampampam/error-pages/internal/pick"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/tarampampam/error-pages/internal/breaker"
|
||||
"github.com/tarampampam/error-pages/internal/cli/shared"
|
||||
"github.com/tarampampam/error-pages/internal/config"
|
||||
"github.com/tarampampam/error-pages/internal/env"
|
||||
appHttp "github.com/tarampampam/error-pages/internal/http"
|
||||
"github.com/tarampampam/error-pages/internal/options"
|
||||
"github.com/tarampampam/error-pages/internal/pick"
|
||||
)
|
||||
|
||||
type command struct {
|
||||
c *cli.Command
|
||||
}
|
||||
|
||||
const (
|
||||
templateNameFlagName = "template-name"
|
||||
defaultErrorPageFlagName = "default-error-page"
|
||||
defaultHTTPCodeFlagName = "default-http-code"
|
||||
showDetailsFlagName = "show-details"
|
||||
proxyHTTPHeadersFlagName = "proxy-headers"
|
||||
disableL10nFlagName = "disable-l10n"
|
||||
)
|
||||
|
||||
const (
|
||||
useRandomTemplate = "random"
|
||||
useRandomTemplateOnEachRequest = "i-said-random"
|
||||
useRandomTemplateDaily = "random-daily"
|
||||
useRandomTemplateHourly = "random-hourly"
|
||||
)
|
||||
|
||||
// NewCommand creates `serve` command.
|
||||
func NewCommand(ctx context.Context, log *zap.Logger, configFile *string) *cobra.Command {
|
||||
var (
|
||||
f flags
|
||||
cfg *config.Config
|
||||
)
|
||||
func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen
|
||||
var cmd = command{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "serve",
|
||||
cmd.c = &cli.Command{
|
||||
Name: "serve",
|
||||
Aliases: []string{"s", "server"},
|
||||
Short: "Start HTTP server",
|
||||
PreRunE: func(cmd *cobra.Command, _ []string) (err error) {
|
||||
if configFile == nil {
|
||||
Usage: "Start HTTP server",
|
||||
Action: func(c *cli.Context) error {
|
||||
var cfg *config.Config
|
||||
|
||||
if configPath := c.String(shared.ConfigFileFlag.Name); configPath == "" { // load config from file
|
||||
return errors.New("path to the config file is required for this command")
|
||||
}
|
||||
|
||||
if err = f.OverrideUsingEnv(cmd.Flags()); err != nil {
|
||||
} else if loadedCfg, err := config.FromYamlFile(c.String(shared.ConfigFileFlag.Name)); err != nil {
|
||||
return err
|
||||
} else {
|
||||
cfg = loadedCfg
|
||||
}
|
||||
|
||||
if cfg, err = config.FromYamlFile(*configFile); err != nil {
|
||||
return err
|
||||
var (
|
||||
ip = c.String(shared.ListenAddrFlag.Name)
|
||||
port = uint16(c.Uint(shared.ListenPortFlag.Name))
|
||||
o options.ErrorPage
|
||||
)
|
||||
|
||||
if net.ParseIP(ip) == nil {
|
||||
return fmt.Errorf("wrong IP address [%s] for listening", ip)
|
||||
}
|
||||
|
||||
return f.Validate()
|
||||
{ // fill options
|
||||
o.Template.Name = c.String(templateNameFlagName)
|
||||
o.L10n.Disabled = c.Bool(disableL10nFlagName)
|
||||
o.Default.PageCode = c.String(defaultErrorPageFlagName)
|
||||
o.Default.HTTPCode = uint16(c.Uint(defaultHTTPCodeFlagName))
|
||||
o.ShowDetails = c.Bool(showDetailsFlagName)
|
||||
|
||||
if headers := c.String(proxyHTTPHeadersFlagName); headers != "" { //nolint:nestif
|
||||
var m = make(map[string]struct{})
|
||||
|
||||
// make unique and ignore empty strings
|
||||
for _, header := range strings.Split(headers, ",") {
|
||||
if h := strings.TrimSpace(header); h != "" {
|
||||
if strings.ContainsRune(h, ' ') {
|
||||
return fmt.Errorf("whitespaces in the HTTP headers for proxying [%s] are not allowed", header)
|
||||
}
|
||||
|
||||
if _, ok := m[h]; !ok {
|
||||
m[h] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// convert map into slice
|
||||
o.ProxyHTTPHeaders = make([]string, 0, len(m))
|
||||
for h := range m {
|
||||
o.ProxyHTTPHeaders = append(o.ProxyHTTPHeaders, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if o.Default.HTTPCode > 599 { //nolint:gomnd
|
||||
return fmt.Errorf("wrong default HTTP response code [%d]", o.Default.HTTPCode)
|
||||
}
|
||||
|
||||
return cmd.Run(c.Context, log, cfg, ip, port, o)
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
shared.ConfigFileFlag,
|
||||
shared.ListenPortFlag,
|
||||
shared.ListenAddrFlag,
|
||||
&cli.StringFlag{
|
||||
Name: templateNameFlagName,
|
||||
Aliases: []string{"t"},
|
||||
Usage: fmt.Sprintf(
|
||||
"template name (set \"%s\" to use a randomized or \"%s\" to use a randomized template on "+
|
||||
"each request or \"%s/%s\" daily/hourly randomized)",
|
||||
useRandomTemplate,
|
||||
useRandomTemplateOnEachRequest,
|
||||
useRandomTemplateDaily,
|
||||
useRandomTemplateHourly,
|
||||
),
|
||||
EnvVars: []string{env.TemplateName.String()},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: defaultErrorPageFlagName,
|
||||
Value: "404",
|
||||
Usage: "default error page",
|
||||
EnvVars: []string{env.DefaultErrorPage.String()},
|
||||
},
|
||||
&cli.UintFlag{
|
||||
Name: defaultHTTPCodeFlagName,
|
||||
Value: 404, //nolint:gomnd
|
||||
Usage: "default HTTP response code",
|
||||
EnvVars: []string{env.DefaultHTTPCode.String()},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: showDetailsFlagName,
|
||||
Usage: "show request details in response",
|
||||
EnvVars: []string{env.ShowDetails.String()},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: proxyHTTPHeadersFlagName,
|
||||
Usage: "proxy HTTP request headers list (comma-separated)",
|
||||
EnvVars: []string{env.ProxyHTTPHeaders.String()},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: disableL10nFlagName,
|
||||
Usage: "disable error pages localization",
|
||||
EnvVars: []string{env.DisableL10n.String()},
|
||||
},
|
||||
},
|
||||
RunE: func(*cobra.Command, []string) error { return run(ctx, log, cfg, f) },
|
||||
}
|
||||
|
||||
f.Init(cmd.Flags())
|
||||
|
||||
return cmd
|
||||
return cmd.c
|
||||
}
|
||||
|
||||
// run current command.
|
||||
func run(parentCtx context.Context, log *zap.Logger, cfg *config.Config, f flags) error { //nolint:funlen
|
||||
// Run current command.
|
||||
func (cmd *command) Run( //nolint:funlen
|
||||
parentCtx context.Context, log *zap.Logger, cfg *config.Config, ip string, port uint16, opt options.ErrorPage,
|
||||
) error {
|
||||
var (
|
||||
ctx, cancel = context.WithCancel(parentCtx) // serve context creation
|
||||
oss = breaker.NewOSSignals(ctx) // OS signals listener
|
||||
@ -70,8 +181,6 @@ func run(parentCtx context.Context, log *zap.Logger, cfg *config.Config, f flags
|
||||
var (
|
||||
templateNames = cfg.TemplateNames()
|
||||
picker interface{ Pick() string }
|
||||
|
||||
opt = f.ToOptions()
|
||||
)
|
||||
|
||||
switch opt.Template.Name {
|
||||
@ -124,8 +233,8 @@ func run(parentCtx context.Context, log *zap.Logger, cfg *config.Config, f flags
|
||||
defer close(errCh)
|
||||
|
||||
log.Info("Server starting",
|
||||
zap.String("addr", f.Listen.IP),
|
||||
zap.Uint16("port", f.Listen.Port),
|
||||
zap.String("addr", ip),
|
||||
zap.Uint16("port", port),
|
||||
zap.String("default error page", opt.Default.PageCode),
|
||||
zap.Uint16("default HTTP response code", opt.Default.HTTPCode),
|
||||
zap.Strings("proxy headers", opt.ProxyHTTPHeaders),
|
||||
@ -133,7 +242,7 @@ func run(parentCtx context.Context, log *zap.Logger, cfg *config.Config, f flags
|
||||
zap.Bool("localization disabled", opt.L10n.Disabled),
|
||||
)
|
||||
|
||||
if err := server.Start(f.Listen.IP, f.Listen.Port); err != nil {
|
||||
if err := server.Start(ip, port); err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
}(startingErrCh)
|
||||
|
@ -1,236 +1,220 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/tarampampam/error-pages/internal/env"
|
||||
"github.com/tarampampam/error-pages/internal/options"
|
||||
)
|
||||
|
||||
type flags struct {
|
||||
Listen struct {
|
||||
IP string
|
||||
Port uint16
|
||||
}
|
||||
template struct {
|
||||
name string
|
||||
}
|
||||
l10n struct {
|
||||
disabled bool
|
||||
}
|
||||
defaultErrorPage string
|
||||
defaultHTTPCode uint16
|
||||
showDetails bool
|
||||
proxyHTTPHeaders string // comma-separated
|
||||
}
|
||||
|
||||
const (
|
||||
listenFlagName = "listen"
|
||||
portFlagName = "port"
|
||||
templateNameFlagName = "template-name"
|
||||
defaultErrorPageFlagName = "default-error-page"
|
||||
defaultHTTPCodeFlagName = "default-http-code"
|
||||
showDetailsFlagName = "show-details"
|
||||
proxyHTTPHeadersFlagName = "proxy-headers"
|
||||
disableL10nFlagName = "disable-l10n"
|
||||
)
|
||||
|
||||
const (
|
||||
useRandomTemplate = "random"
|
||||
useRandomTemplateOnEachRequest = "i-said-random"
|
||||
useRandomTemplateDaily = "random-daily"
|
||||
useRandomTemplateHourly = "random-hourly"
|
||||
)
|
||||
|
||||
func (f *flags) Init(flagSet *pflag.FlagSet) {
|
||||
flagSet.StringVarP(
|
||||
&f.Listen.IP,
|
||||
listenFlagName, "l",
|
||||
"0.0.0.0",
|
||||
fmt.Sprintf("IP address to Listen on [$%s]", env.ListenAddr),
|
||||
)
|
||||
flagSet.Uint16VarP(
|
||||
&f.Listen.Port,
|
||||
portFlagName, "p",
|
||||
8080, //nolint:gomnd // must be same as default healthcheck `--port` flag value
|
||||
fmt.Sprintf("TCP prt number [$%s]", env.ListenPort),
|
||||
)
|
||||
flagSet.StringVarP(
|
||||
&f.template.name,
|
||||
templateNameFlagName, "t",
|
||||
"",
|
||||
fmt.Sprintf(
|
||||
"template name (set \"%s\" to use a randomized or \"%s\" to use a randomized template on each request "+
|
||||
"or \"%s/%s\" daily/hourly randomized) [$%s]",
|
||||
useRandomTemplate,
|
||||
useRandomTemplateOnEachRequest,
|
||||
useRandomTemplateDaily,
|
||||
useRandomTemplateHourly,
|
||||
env.TemplateName,
|
||||
),
|
||||
)
|
||||
flagSet.StringVarP(
|
||||
&f.defaultErrorPage,
|
||||
defaultErrorPageFlagName, "",
|
||||
"404",
|
||||
fmt.Sprintf("default error page [$%s]", env.DefaultErrorPage),
|
||||
)
|
||||
flagSet.Uint16VarP(
|
||||
&f.defaultHTTPCode,
|
||||
defaultHTTPCodeFlagName, "",
|
||||
404, //nolint:gomnd
|
||||
fmt.Sprintf("default HTTP response code [$%s]", env.DefaultHTTPCode),
|
||||
)
|
||||
flagSet.BoolVarP(
|
||||
&f.showDetails,
|
||||
showDetailsFlagName, "",
|
||||
false,
|
||||
fmt.Sprintf("show request details in response [$%s]", env.ShowDetails),
|
||||
)
|
||||
flagSet.StringVarP(
|
||||
&f.proxyHTTPHeaders,
|
||||
proxyHTTPHeadersFlagName, "",
|
||||
"",
|
||||
fmt.Sprintf("proxy HTTP request headers list (comma-separated) [$%s]", env.ProxyHTTPHeaders),
|
||||
)
|
||||
flagSet.BoolVarP(
|
||||
&f.l10n.disabled,
|
||||
disableL10nFlagName, "",
|
||||
false,
|
||||
fmt.Sprintf("disable error pages localization [$%s]", env.DisableL10n),
|
||||
)
|
||||
}
|
||||
|
||||
func (f *flags) OverrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) { //nolint:gocognit,gocyclo
|
||||
flagSet.VisitAll(func(flag *pflag.Flag) {
|
||||
// flag was NOT defined using CLI (flags should have maximal priority)
|
||||
if !flag.Changed { //nolint:nestif
|
||||
switch flag.Name {
|
||||
case listenFlagName:
|
||||
if envVar, exists := env.ListenAddr.Lookup(); exists {
|
||||
f.Listen.IP = strings.TrimSpace(envVar)
|
||||
}
|
||||
|
||||
case portFlagName:
|
||||
if envVar, exists := env.ListenPort.Lookup(); exists {
|
||||
if p, err := strconv.ParseUint(envVar, 10, 16); err == nil {
|
||||
f.Listen.Port = uint16(p)
|
||||
} else {
|
||||
lastErr = fmt.Errorf("wrong TCP port environment variable [%s] value", envVar)
|
||||
}
|
||||
}
|
||||
|
||||
case templateNameFlagName:
|
||||
if envVar, exists := env.TemplateName.Lookup(); exists {
|
||||
f.template.name = strings.TrimSpace(envVar)
|
||||
}
|
||||
|
||||
case defaultErrorPageFlagName:
|
||||
if envVar, exists := env.DefaultErrorPage.Lookup(); exists {
|
||||
f.defaultErrorPage = strings.TrimSpace(envVar)
|
||||
}
|
||||
|
||||
case defaultHTTPCodeFlagName:
|
||||
if envVar, exists := env.DefaultHTTPCode.Lookup(); exists {
|
||||
if code, err := strconv.ParseUint(envVar, 10, 16); err == nil {
|
||||
f.defaultHTTPCode = uint16(code)
|
||||
} else {
|
||||
lastErr = fmt.Errorf("wrong default HTTP response code environment variable [%s] value", envVar)
|
||||
}
|
||||
}
|
||||
|
||||
case showDetailsFlagName:
|
||||
if envVar, exists := env.ShowDetails.Lookup(); exists {
|
||||
if b, err := strconv.ParseBool(envVar); err == nil {
|
||||
f.showDetails = b
|
||||
}
|
||||
}
|
||||
|
||||
case proxyHTTPHeadersFlagName:
|
||||
if envVar, exists := env.ProxyHTTPHeaders.Lookup(); exists {
|
||||
f.proxyHTTPHeaders = strings.TrimSpace(envVar)
|
||||
}
|
||||
|
||||
case disableL10nFlagName:
|
||||
if envVar, exists := env.DisableL10n.Lookup(); exists {
|
||||
if b, err := strconv.ParseBool(envVar); err == nil {
|
||||
f.l10n.disabled = b
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (f *flags) Validate() error {
|
||||
if net.ParseIP(f.Listen.IP) == nil {
|
||||
return fmt.Errorf("wrong IP address [%s] for listening", f.Listen.IP)
|
||||
}
|
||||
|
||||
if f.defaultHTTPCode > 599 { //nolint:gomnd
|
||||
return fmt.Errorf("wrong default HTTP response code [%d]", f.defaultHTTPCode)
|
||||
}
|
||||
|
||||
if strings.ContainsRune(f.proxyHTTPHeaders, ' ') {
|
||||
return fmt.Errorf("whitespaces in the HTTP headers for proxying [%s] are not allowed", f.proxyHTTPHeaders)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// headersToProxy converts a comma-separated string with headers list into strings slice (with a sorting and without
|
||||
// duplicates).
|
||||
func (f *flags) headersToProxy() []string {
|
||||
var raw = strings.Split(f.proxyHTTPHeaders, ",")
|
||||
|
||||
if len(raw) == 0 {
|
||||
return []string{}
|
||||
} else if len(raw) == 1 {
|
||||
if h := strings.TrimSpace(raw[0]); h != "" {
|
||||
return []string{h}
|
||||
} else {
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
var m = make(map[string]struct{}, len(raw))
|
||||
|
||||
// make unique and ignore empty strings
|
||||
for _, h := range raw {
|
||||
if h = strings.TrimSpace(h); h != "" {
|
||||
if _, ok := m[h]; !ok {
|
||||
m[h] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// convert map into slice
|
||||
var headers = make([]string, 0, len(m))
|
||||
for h := range m {
|
||||
headers = append(headers, h)
|
||||
}
|
||||
|
||||
// make sort
|
||||
sort.Strings(headers)
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
func (f *flags) ToOptions() (o options.ErrorPage) {
|
||||
o.Default.PageCode = f.defaultErrorPage
|
||||
o.Default.HTTPCode = f.defaultHTTPCode
|
||||
o.L10n.Disabled = f.l10n.disabled
|
||||
o.Template.Name = f.template.name
|
||||
o.ShowDetails = f.showDetails
|
||||
o.ProxyHTTPHeaders = f.headersToProxy()
|
||||
|
||||
return o
|
||||
}
|
||||
// import (
|
||||
// "fmt"
|
||||
// "net"
|
||||
// "sort"
|
||||
// "strconv"
|
||||
// "strings"
|
||||
//
|
||||
// "github.com/spf13/pflag"
|
||||
//
|
||||
// "github.com/tarampampam/error-pages/internal/env"
|
||||
// "github.com/tarampampam/error-pages/internal/options"
|
||||
// )
|
||||
//
|
||||
// type flags struct {
|
||||
// Listen struct {
|
||||
// IP string
|
||||
// Port uint16
|
||||
// }
|
||||
// template struct {
|
||||
// name string
|
||||
// }
|
||||
// l10n struct {
|
||||
// disabled bool
|
||||
// }
|
||||
// defaultErrorPage string
|
||||
// defaultHTTPCode uint16
|
||||
// showDetails bool
|
||||
// proxyHTTPHeaders string // comma-separated
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
// func (f *flags) Init(flagSet *pflag.FlagSet) {
|
||||
// flagSet.StringVarP(
|
||||
// &f.Listen.IP,
|
||||
// listenFlagName, "l",
|
||||
// "0.0.0.0",
|
||||
// fmt.Sprintf("IP address to Listen on [$%s]", env.ListenAddr),
|
||||
// )
|
||||
// flagSet.Uint16VarP(
|
||||
// &f.Listen.Port,
|
||||
// portFlagName, "p",
|
||||
// 8080, //nolint:gomnd // must be same as default healthcheck `--port` flag value
|
||||
// fmt.Sprintf("TCP prt number [$%s]", env.ListenPort),
|
||||
// )
|
||||
// flagSet.StringVarP(
|
||||
// &f.template.name,
|
||||
// templateNameFlagName, "t",
|
||||
// "",
|
||||
// fmt.Sprintf(
|
||||
// "template name (set \"%s\" to use a randomized or \"%s\" to use a randomized template on each request "+
|
||||
// "or \"%s/%s\" daily/hourly randomized) [$%s]",
|
||||
// useRandomTemplate,
|
||||
// useRandomTemplateOnEachRequest,
|
||||
// useRandomTemplateDaily,
|
||||
// useRandomTemplateHourly,
|
||||
// env.TemplateName,
|
||||
// ),
|
||||
// )
|
||||
// flagSet.StringVarP(
|
||||
// &f.defaultErrorPage,
|
||||
// defaultErrorPageFlagName, "",
|
||||
// "404",
|
||||
// fmt.Sprintf("default error page [$%s]", env.DefaultErrorPage),
|
||||
// )
|
||||
// flagSet.Uint16VarP(
|
||||
// &f.defaultHTTPCode,
|
||||
// defaultHTTPCodeFlagName, "",
|
||||
// 404, //nolint:gomnd
|
||||
// fmt.Sprintf("default HTTP response code [$%s]", env.DefaultHTTPCode),
|
||||
// )
|
||||
// flagSet.BoolVarP(
|
||||
// &f.showDetails,
|
||||
// showDetailsFlagName, "",
|
||||
// false,
|
||||
// fmt.Sprintf("show request details in response [$%s]", env.ShowDetails),
|
||||
// )
|
||||
// flagSet.StringVarP(
|
||||
// &f.proxyHTTPHeaders,
|
||||
// proxyHTTPHeadersFlagName, "",
|
||||
// "",
|
||||
// fmt.Sprintf("proxy HTTP request headers list (comma-separated) [$%s]", env.ProxyHTTPHeaders),
|
||||
// )
|
||||
// flagSet.BoolVarP(
|
||||
// &f.l10n.disabled,
|
||||
// disableL10nFlagName, "",
|
||||
// false,
|
||||
// fmt.Sprintf("disable error pages localization [$%s]", env.DisableL10n),
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// func (f *flags) OverrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) { //nolint:gocognit,gocyclo
|
||||
// flagSet.VisitAll(func(flag *pflag.Flag) {
|
||||
// // flag was NOT defined using CLI (flags should have maximal priority)
|
||||
// if !flag.Changed { //nolint:nestif
|
||||
// switch flag.Name {
|
||||
// case listenFlagName:
|
||||
// if envVar, exists := env.ListenAddr.Lookup(); exists {
|
||||
// f.Listen.IP = strings.TrimSpace(envVar)
|
||||
// }
|
||||
//
|
||||
// case portFlagName:
|
||||
// if envVar, exists := env.ListenPort.Lookup(); exists {
|
||||
// if p, err := strconv.ParseUint(envVar, 10, 16); err == nil {
|
||||
// f.Listen.Port = uint16(p)
|
||||
// } else {
|
||||
// lastErr = fmt.Errorf("wrong TCP port environment variable [%s] value", envVar)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// case templateNameFlagName:
|
||||
// if envVar, exists := env.TemplateName.Lookup(); exists {
|
||||
// f.template.name = strings.TrimSpace(envVar)
|
||||
// }
|
||||
//
|
||||
// case defaultErrorPageFlagName:
|
||||
// if envVar, exists := env.DefaultErrorPage.Lookup(); exists {
|
||||
// f.defaultErrorPage = strings.TrimSpace(envVar)
|
||||
// }
|
||||
//
|
||||
// case defaultHTTPCodeFlagName:
|
||||
// if envVar, exists := env.DefaultHTTPCode.Lookup(); exists {
|
||||
// if code, err := strconv.ParseUint(envVar, 10, 16); err == nil {
|
||||
// f.defaultHTTPCode = uint16(code)
|
||||
// } else {
|
||||
// lastErr = fmt.Errorf("wrong default HTTP response code environment variable [%s] value", envVar)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// case showDetailsFlagName:
|
||||
// if envVar, exists := env.ShowDetails.Lookup(); exists {
|
||||
// if b, err := strconv.ParseBool(envVar); err == nil {
|
||||
// f.showDetails = b
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// case proxyHTTPHeadersFlagName:
|
||||
// if envVar, exists := env.ProxyHTTPHeaders.Lookup(); exists {
|
||||
// f.proxyHTTPHeaders = strings.TrimSpace(envVar)
|
||||
// }
|
||||
//
|
||||
// case disableL10nFlagName:
|
||||
// if envVar, exists := env.DisableL10n.Lookup(); exists {
|
||||
// if b, err := strconv.ParseBool(envVar); err == nil {
|
||||
// f.l10n.disabled = b
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// return lastErr
|
||||
// }
|
||||
//
|
||||
// func (f *flags) Validate() error {
|
||||
// if net.ParseIP(f.Listen.IP) == nil {
|
||||
// return fmt.Errorf("wrong IP address [%s] for listening", f.Listen.IP)
|
||||
// }
|
||||
//
|
||||
// if f.defaultHTTPCode > 599 { //nolint:gomnd
|
||||
// return fmt.Errorf("wrong default HTTP response code [%d]", f.defaultHTTPCode)
|
||||
// }
|
||||
//
|
||||
// if strings.ContainsRune(f.proxyHTTPHeaders, ' ') {
|
||||
// return fmt.Errorf("whitespaces in the HTTP headers for proxying [%s] are not allowed", f.proxyHTTPHeaders)
|
||||
// }
|
||||
//
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// // headersToProxy converts a comma-separated string with headers list into strings slice (with a sorting and without
|
||||
// // duplicates).
|
||||
// func (f *flags) headersToProxy() []string {
|
||||
// var raw = strings.Split(f.proxyHTTPHeaders, ",")
|
||||
//
|
||||
// if len(raw) == 0 {
|
||||
// return []string{}
|
||||
// } else if len(raw) == 1 {
|
||||
// if h := strings.TrimSpace(raw[0]); h != "" {
|
||||
// return []string{h}
|
||||
// } else {
|
||||
// return []string{}
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// var m = make(map[string]struct{}, len(raw))
|
||||
//
|
||||
// // make unique and ignore empty strings
|
||||
// for _, h := range raw {
|
||||
// if h = strings.TrimSpace(h); h != "" {
|
||||
// if _, ok := m[h]; !ok {
|
||||
// m[h] = struct{}{}
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // convert map into slice
|
||||
// var headers = make([]string, 0, len(m))
|
||||
// for h := range m {
|
||||
// headers = append(headers, h)
|
||||
// }
|
||||
//
|
||||
// // make sort
|
||||
// sort.Strings(headers)
|
||||
//
|
||||
// return headers
|
||||
// }
|
||||
//
|
||||
// func (f *flags) ToOptions() (o options.ErrorPage) {
|
||||
// o.Default.PageCode = f.defaultErrorPage
|
||||
// o.Default.HTTPCode = f.defaultHTTPCode
|
||||
// o.L10n.Disabled = f.l10n.disabled
|
||||
// o.Template.Name = f.template.name
|
||||
// o.ShowDetails = f.showDetails
|
||||
// o.ProxyHTTPHeaders = f.headersToProxy()
|
||||
//
|
||||
// return o
|
||||
// }
|
||||
|
31
internal/cli/shared/flags.go
Normal file
31
internal/cli/shared/flags.go
Normal file
@ -0,0 +1,31 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/tarampampam/error-pages/internal/env"
|
||||
)
|
||||
|
||||
var ConfigFileFlag = &cli.StringFlag{ //nolint:gochecknoglobals
|
||||
Name: "config-file",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "path to the config file (yaml)",
|
||||
Value: "./error-pages.yml",
|
||||
EnvVars: []string{env.ConfigFilePath.String()},
|
||||
}
|
||||
|
||||
var ListenAddrFlag = &cli.StringFlag{ //nolint:gochecknoglobals
|
||||
Name: "listen",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "IP address to Listen on",
|
||||
Value: "0.0.0.0",
|
||||
EnvVars: []string{env.ListenAddr.String()},
|
||||
}
|
||||
|
||||
var ListenPortFlag = &cli.UintFlag{ //nolint:gochecknoglobals
|
||||
Name: "port",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "TCP port number",
|
||||
Value: 8080, //nolint:gomnd
|
||||
EnvVars: []string{env.ListenPort.String()},
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
// Package version contains CLI `version` command implementation.
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewCommand creates `version` command.
|
||||
func NewCommand(ver string) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "version",
|
||||
Aliases: []string{"v", "ver"},
|
||||
Short: "Display application version",
|
||||
RunE: func(*cobra.Command, []string) (err error) {
|
||||
_, err = fmt.Fprintf(os.Stdout, "app version:\t%s (%s)\n", ver, runtime.Version())
|
||||
|
||||
return
|
||||
},
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package version_test
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/kami-zh/go-capturer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/cli/version"
|
||||
)
|
||||
|
||||
func TestProperties(t *testing.T) {
|
||||
cmd := version.NewCommand("")
|
||||
|
||||
assert.Equal(t, "version", cmd.Use)
|
||||
assert.ElementsMatch(t, []string{"v", "ver"}, cmd.Aliases)
|
||||
assert.NotNil(t, cmd.RunE)
|
||||
}
|
||||
|
||||
func TestCommandRun(t *testing.T) {
|
||||
cmd := version.NewCommand("1.2.3@foobar")
|
||||
cmd.SetArgs([]string{})
|
||||
|
||||
output := capturer.CaptureStdout(func() {
|
||||
assert.NoError(t, cmd.Execute())
|
||||
})
|
||||
|
||||
assert.Contains(t, output, "1.2.3@foobar")
|
||||
assert.Contains(t, output, runtime.Version())
|
||||
}
|
3
internal/env/env.go
vendored
3
internal/env/env.go
vendored
@ -6,6 +6,9 @@ import "os"
|
||||
type envVariable string
|
||||
|
||||
const (
|
||||
LogLevel envVariable = "LOG_LEVEL" // logging level
|
||||
LogFormat envVariable = "LOG_FORMAT" // logging format (json|console)
|
||||
|
||||
ListenAddr envVariable = "LISTEN_ADDR" // IP address for listening
|
||||
ListenPort envVariable = "LISTEN_PORT" // port number for listening
|
||||
TemplateName envVariable = "TEMPLATE_NAME" // template name
|
||||
|
68
internal/logger/format.go
Normal file
68
internal/logger/format.go
Normal file
@ -0,0 +1,68 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A Format is a logging format.
|
||||
type Format uint8
|
||||
|
||||
const (
|
||||
ConsoleFormat Format = iota // useful for console output (for humans)
|
||||
JSONFormat // useful for logging aggregation systems (for robots)
|
||||
)
|
||||
|
||||
// String returns a lower-case ASCII representation of the log format.
|
||||
func (f Format) String() string {
|
||||
switch f {
|
||||
case ConsoleFormat:
|
||||
return "console"
|
||||
case JSONFormat:
|
||||
return "json"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("format(%d)", f)
|
||||
}
|
||||
|
||||
// Formats returns a slice of all logging formats.
|
||||
func Formats() []Format {
|
||||
return []Format{ConsoleFormat, JSONFormat}
|
||||
}
|
||||
|
||||
// FormatStrings returns a slice of all logging formats as strings.
|
||||
func FormatStrings() []string {
|
||||
var (
|
||||
formats = Formats()
|
||||
result = make([]string, len(formats))
|
||||
)
|
||||
|
||||
for i := range formats {
|
||||
result[i] = formats[i].String()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ParseFormat parses a format (case is ignored) based on the ASCII representation of the log format.
|
||||
// If the provided ASCII representation is invalid an error is returned.
|
||||
//
|
||||
// This is particularly useful when dealing with text input to configure log formats.
|
||||
func ParseFormat[T string | []byte](text T) (Format, error) {
|
||||
var format string
|
||||
|
||||
if s, ok := any(text).(string); ok {
|
||||
format = s
|
||||
} else {
|
||||
format = string(any(text).([]byte))
|
||||
}
|
||||
|
||||
switch strings.ToLower(format) {
|
||||
case "console", "": // make the zero value useful
|
||||
return ConsoleFormat, nil
|
||||
case "json":
|
||||
return JSONFormat, nil
|
||||
}
|
||||
|
||||
return Format(0), fmt.Errorf("unrecognized logging format: %q", text)
|
||||
}
|
62
internal/logger/format_test.go
Normal file
62
internal/logger/format_test.go
Normal file
@ -0,0 +1,62 @@
|
||||
package logger_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/tarampampam/error-pages/internal/logger"
|
||||
)
|
||||
|
||||
func TestFormat_String(t *testing.T) {
|
||||
for name, tt := range map[string]struct {
|
||||
giveFormat logger.Format
|
||||
wantString string
|
||||
}{
|
||||
"json": {giveFormat: logger.JSONFormat, wantString: "json"},
|
||||
"console": {giveFormat: logger.ConsoleFormat, wantString: "console"},
|
||||
"<unknown>": {giveFormat: logger.Format(255), wantString: "format(255)"},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
require.Equal(t, tt.wantString, tt.giveFormat.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFormat(t *testing.T) {
|
||||
for name, tt := range map[string]struct {
|
||||
giveBytes []byte
|
||||
giveString string
|
||||
wantFormat logger.Format
|
||||
wantError error
|
||||
}{
|
||||
"<empty value>": {giveBytes: []byte(""), wantFormat: logger.ConsoleFormat},
|
||||
"<empty value> (string)": {giveString: "", wantFormat: logger.ConsoleFormat},
|
||||
"console": {giveBytes: []byte("console"), wantFormat: logger.ConsoleFormat},
|
||||
"console (string)": {giveString: "console", wantFormat: logger.ConsoleFormat},
|
||||
"json": {giveBytes: []byte("json"), wantFormat: logger.JSONFormat},
|
||||
"json (string)": {giveString: "json", wantFormat: logger.JSONFormat},
|
||||
"foobar": {giveBytes: []byte("foobar"), wantError: errors.New("unrecognized logging format: \"foobar\"")}, //nolint:lll
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var (
|
||||
f logger.Format
|
||||
err error
|
||||
)
|
||||
|
||||
if tt.giveString != "" {
|
||||
f, err = logger.ParseFormat(tt.giveString)
|
||||
} else {
|
||||
f, err = logger.ParseFormat(tt.giveBytes)
|
||||
}
|
||||
|
||||
if tt.wantError == nil {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.wantFormat, f)
|
||||
} else {
|
||||
require.EqualError(t, err, tt.wantError.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
83
internal/logger/level.go
Normal file
83
internal/logger/level.go
Normal file
@ -0,0 +1,83 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A Level is a logging level.
|
||||
type Level int8
|
||||
|
||||
const (
|
||||
DebugLevel Level = iota - 1
|
||||
InfoLevel // default level (zero-value)
|
||||
WarnLevel
|
||||
ErrorLevel
|
||||
FatalLevel
|
||||
)
|
||||
|
||||
// String returns a lower-case ASCII representation of the log level.
|
||||
func (l Level) String() string {
|
||||
switch l {
|
||||
case DebugLevel:
|
||||
return "debug"
|
||||
case InfoLevel:
|
||||
return "info"
|
||||
case WarnLevel:
|
||||
return "warn"
|
||||
case ErrorLevel:
|
||||
return "error"
|
||||
case FatalLevel:
|
||||
return "fatal"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("level(%d)", l)
|
||||
}
|
||||
|
||||
// Levels returns a slice of all logging levels.
|
||||
func Levels() []Level {
|
||||
return []Level{DebugLevel, InfoLevel, WarnLevel, ErrorLevel, FatalLevel}
|
||||
}
|
||||
|
||||
// LevelStrings returns a slice of all logging levels as strings.
|
||||
func LevelStrings() []string {
|
||||
var (
|
||||
levels = Levels()
|
||||
result = make([]string, len(levels))
|
||||
)
|
||||
|
||||
for i := range levels {
|
||||
result[i] = levels[i].String()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ParseLevel parses a level (case is ignored) based on the ASCII representation of the log level.
|
||||
// If the provided ASCII representation is invalid an error is returned.
|
||||
//
|
||||
// This is particularly useful when dealing with text input to configure log levels.
|
||||
func ParseLevel[T string | []byte](text T) (Level, error) {
|
||||
var lvl string
|
||||
|
||||
if s, ok := any(text).(string); ok {
|
||||
lvl = s
|
||||
} else {
|
||||
lvl = string(any(text).([]byte))
|
||||
}
|
||||
|
||||
switch strings.ToLower(lvl) {
|
||||
case "debug", "verbose", "trace":
|
||||
return DebugLevel, nil
|
||||
case "info", "": // make the zero value useful
|
||||
return InfoLevel, nil
|
||||
case "warn":
|
||||
return WarnLevel, nil
|
||||
case "error":
|
||||
return ErrorLevel, nil
|
||||
case "fatal":
|
||||
return FatalLevel, nil
|
||||
}
|
||||
|
||||
return Level(0), fmt.Errorf("unrecognized logging level: %q", text)
|
||||
}
|
84
internal/logger/level_test.go
Normal file
84
internal/logger/level_test.go
Normal file
@ -0,0 +1,84 @@
|
||||
package logger_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/tarampampam/error-pages/internal/logger"
|
||||
)
|
||||
|
||||
func TestLevel_String(t *testing.T) {
|
||||
for name, tt := range map[string]struct {
|
||||
giveLevel logger.Level
|
||||
wantString string
|
||||
}{
|
||||
"debug": {giveLevel: logger.DebugLevel, wantString: "debug"},
|
||||
"info": {giveLevel: logger.InfoLevel, wantString: "info"},
|
||||
"warn": {giveLevel: logger.WarnLevel, wantString: "warn"},
|
||||
"error": {giveLevel: logger.ErrorLevel, wantString: "error"},
|
||||
"fatal": {giveLevel: logger.FatalLevel, wantString: "fatal"},
|
||||
"<unknown>": {giveLevel: logger.Level(127), wantString: "level(127)"},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
require.Equal(t, tt.wantString, tt.giveLevel.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLevel(t *testing.T) {
|
||||
for name, tt := range map[string]struct {
|
||||
giveBytes []byte
|
||||
giveString string
|
||||
wantLevel logger.Level
|
||||
wantError error
|
||||
}{
|
||||
"<empty value>": {giveBytes: []byte(""), wantLevel: logger.InfoLevel},
|
||||
"<empty value> (string)": {giveString: "", wantLevel: logger.InfoLevel},
|
||||
"trace": {giveBytes: []byte("debug"), wantLevel: logger.DebugLevel},
|
||||
"verbose": {giveBytes: []byte("debug"), wantLevel: logger.DebugLevel},
|
||||
"debug": {giveBytes: []byte("debug"), wantLevel: logger.DebugLevel},
|
||||
"debug (string)": {giveString: "debug", wantLevel: logger.DebugLevel},
|
||||
"info": {giveBytes: []byte("info"), wantLevel: logger.InfoLevel},
|
||||
"warn": {giveBytes: []byte("warn"), wantLevel: logger.WarnLevel},
|
||||
"error": {giveBytes: []byte("error"), wantLevel: logger.ErrorLevel},
|
||||
"fatal": {giveBytes: []byte("fatal"), wantLevel: logger.FatalLevel},
|
||||
"fatal (string)": {giveString: "fatal", wantLevel: logger.FatalLevel},
|
||||
"foobar": {giveBytes: []byte("foobar"), wantError: errors.New("unrecognized logging level: \"foobar\"")}, //nolint:lll
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var (
|
||||
l logger.Level
|
||||
err error
|
||||
)
|
||||
|
||||
if tt.giveString != "" {
|
||||
l, err = logger.ParseLevel(tt.giveString)
|
||||
} else {
|
||||
l, err = logger.ParseLevel(tt.giveBytes)
|
||||
}
|
||||
|
||||
if tt.wantError == nil {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.wantLevel, l)
|
||||
} else {
|
||||
require.EqualError(t, err, tt.wantError.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLevels(t *testing.T) {
|
||||
require.Equal(t, []logger.Level{
|
||||
logger.DebugLevel,
|
||||
logger.InfoLevel,
|
||||
logger.WarnLevel,
|
||||
logger.ErrorLevel,
|
||||
logger.FatalLevel,
|
||||
}, logger.Levels())
|
||||
}
|
||||
|
||||
func TestLevelStrings(t *testing.T) {
|
||||
require.Equal(t, []string{"debug", "info", "warn", "error", "fatal"}, logger.LevelStrings())
|
||||
}
|
@ -2,20 +2,27 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// New creates new "zap" logger with little customization.
|
||||
func New(verbose, debug, logJSON bool) (*zap.Logger, error) {
|
||||
// New creates new "zap" logger with a small customization.
|
||||
func New(l Level, f Format) (*zap.Logger, error) {
|
||||
var config zap.Config
|
||||
|
||||
if logJSON {
|
||||
config = zap.NewProductionConfig()
|
||||
} else {
|
||||
switch f {
|
||||
case ConsoleFormat:
|
||||
config = zap.NewDevelopmentConfig()
|
||||
config.EncoderConfig.EncodeLevel = zapcore.LowercaseColorLevelEncoder
|
||||
config.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("15:04:05")
|
||||
|
||||
case JSONFormat:
|
||||
config = zap.NewProductionConfig() // json encoder is used by default
|
||||
|
||||
default:
|
||||
return nil, errors.New("unsupported logging format")
|
||||
}
|
||||
|
||||
// default configuration for all encoders
|
||||
@ -24,15 +31,31 @@ func New(verbose, debug, logJSON bool) (*zap.Logger, error) {
|
||||
config.DisableStacktrace = true
|
||||
config.DisableCaller = true
|
||||
|
||||
if debug {
|
||||
// enable additional features for debugging
|
||||
if l <= DebugLevel {
|
||||
config.Development = true
|
||||
config.DisableStacktrace = false
|
||||
config.DisableCaller = false
|
||||
}
|
||||
|
||||
if verbose || debug {
|
||||
config.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
|
||||
var zapLvl zapcore.Level
|
||||
|
||||
switch l { // convert level to zap.Level
|
||||
case DebugLevel:
|
||||
zapLvl = zap.DebugLevel
|
||||
case InfoLevel:
|
||||
zapLvl = zap.InfoLevel
|
||||
case WarnLevel:
|
||||
zapLvl = zap.WarnLevel
|
||||
case ErrorLevel:
|
||||
zapLvl = zap.ErrorLevel
|
||||
case FatalLevel:
|
||||
zapLvl = zap.FatalLevel
|
||||
default:
|
||||
return nil, errors.New("unsupported logging level")
|
||||
}
|
||||
|
||||
config.Level = zap.NewAtomicLevelAt(zapLvl)
|
||||
|
||||
return config.Build()
|
||||
}
|
||||
|
@ -8,64 +8,51 @@ import (
|
||||
|
||||
"github.com/kami-zh/go-capturer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/tarampampam/error-pages/internal/logger"
|
||||
)
|
||||
|
||||
func TestNewNotVerboseDebugJSON(t *testing.T) {
|
||||
func TestNewDebugLevelConsoleFormat(t *testing.T) {
|
||||
output := capturer.CaptureStderr(func() {
|
||||
log, err := logger.New(false, false, false)
|
||||
assert.NoError(t, err)
|
||||
log, err := logger.New(logger.DebugLevel, logger.ConsoleFormat)
|
||||
require.NoError(t, err)
|
||||
|
||||
log.Info("inf msg")
|
||||
log.Debug("dbg msg")
|
||||
log.Info("inf msg")
|
||||
log.Error("err msg")
|
||||
})
|
||||
|
||||
assert.Contains(t, output, time.Now().Format("15:04:05"))
|
||||
assert.Regexp(t, `\t.+info.+\tinf msg`, output)
|
||||
assert.NotContains(t, output, "dbg msg")
|
||||
assert.Contains(t, output, "err msg")
|
||||
}
|
||||
|
||||
func TestNewVerboseNotDebugJSON(t *testing.T) {
|
||||
output := capturer.CaptureStderr(func() {
|
||||
log, err := logger.New(true, false, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
log.Info("inf msg")
|
||||
log.Debug("dbg msg")
|
||||
log.Error("err msg")
|
||||
})
|
||||
|
||||
assert.Contains(t, output, time.Now().Format("15:04:05"))
|
||||
assert.Regexp(t, `\t.+info.+\tinf msg`, output)
|
||||
assert.Contains(t, output, "dbg msg")
|
||||
assert.Contains(t, output, "err msg")
|
||||
}
|
||||
|
||||
func TestNewVerboseDebugNotJSON(t *testing.T) {
|
||||
output := capturer.CaptureStderr(func() {
|
||||
log, err := logger.New(true, true, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
log.Info("inf msg")
|
||||
log.Debug("dbg msg")
|
||||
log.Error("err msg")
|
||||
})
|
||||
|
||||
assert.Contains(t, output, time.Now().Format("15:04:05"))
|
||||
assert.Regexp(t, `\t.+info.+\t.+logger_test\.go:\d+\tinf msg`, output)
|
||||
assert.Contains(t, output, "dbg msg")
|
||||
assert.Contains(t, output, "err msg")
|
||||
}
|
||||
|
||||
func TestNewNotVerboseDebugButJSON(t *testing.T) {
|
||||
func TestNewErrorLevelConsoleFormat(t *testing.T) {
|
||||
output := capturer.CaptureStderr(func() {
|
||||
log, err := logger.New(false, false, true)
|
||||
assert.NoError(t, err)
|
||||
log, err := logger.New(logger.ErrorLevel, logger.ConsoleFormat)
|
||||
require.NoError(t, err)
|
||||
|
||||
log.Info("inf msg")
|
||||
log.Debug("dbg msg")
|
||||
log.Info("inf msg")
|
||||
log.Error("err msg")
|
||||
})
|
||||
|
||||
assert.NotContains(t, output, "inf msg")
|
||||
assert.NotContains(t, output, "dbg msg")
|
||||
assert.Contains(t, output, "err msg")
|
||||
}
|
||||
|
||||
func TestNewWarnLevelJSONFormat(t *testing.T) {
|
||||
output := capturer.CaptureStderr(func() {
|
||||
log, err := logger.New(logger.WarnLevel, logger.JSONFormat)
|
||||
require.NoError(t, err)
|
||||
|
||||
log.Debug("dbg msg")
|
||||
log.Info("inf msg")
|
||||
log.Warn("warn msg")
|
||||
log.Error("err msg")
|
||||
})
|
||||
|
||||
@ -75,6 +62,14 @@ func TestNewNotVerboseDebugButJSON(t *testing.T) {
|
||||
|
||||
lines := strings.Split(strings.Trim(output, "\n"), "\n")
|
||||
|
||||
assert.JSONEq(t, `{"level":"info","ts":0.1,"msg":"inf msg"}`, lines[0])
|
||||
assert.JSONEq(t, `{"level":"warn","ts":0.1,"msg":"warn msg"}`, lines[0])
|
||||
assert.JSONEq(t, `{"level":"error","ts":0.1,"msg":"err msg"}`, lines[1])
|
||||
}
|
||||
|
||||
func TestNewErrors(t *testing.T) {
|
||||
_, err := logger.New(logger.Level(127), logger.ConsoleFormat)
|
||||
require.EqualError(t, err, "unsupported logging level")
|
||||
|
||||
_, err = logger.New(logger.WarnLevel, logger.Format(255))
|
||||
require.EqualError(t, err, "unsupported logging format")
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user