mirror of
https://github.com/tarampampam/error-pages.git
synced 2024-08-30 18:22:40 +00:00
Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
22d3e3485e | |||
375272b561 | |||
7e7f956fae |
@ -4,6 +4,14 @@ 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].
|
||||
|
||||
## v2.6.0
|
||||
|
||||
### Added
|
||||
|
||||
- Possibility to change the template to the random once a day using "special" template name `random-daily` (or hourly, using `random-hourly`) [#48]
|
||||
|
||||
[#48]:https://github.com/tarampampam/error-pages/issues/48
|
||||
|
||||
## v2.5.0
|
||||
|
||||
### Changed
|
||||
|
@ -69,7 +69,7 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config
|
||||
|
||||
var (
|
||||
templateNames = cfg.TemplateNames()
|
||||
picker *pick.StringsSlice
|
||||
picker interface{ Pick() string }
|
||||
)
|
||||
|
||||
switch f.template.name {
|
||||
@ -83,6 +83,16 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config
|
||||
|
||||
picker = pick.NewStringsSlice(templateNames, pick.RandomEveryTime)
|
||||
|
||||
case useRandomTemplateDaily:
|
||||
log.Info("A random template will be used and changed once a day")
|
||||
|
||||
picker = pick.NewStringsSliceWithInterval(templateNames, pick.RandomEveryTime, time.Hour*24) //nolint:gomnd
|
||||
|
||||
case useRandomTemplateHourly:
|
||||
log.Info("A random template will be used and changed hourly")
|
||||
|
||||
picker = pick.NewStringsSliceWithInterval(templateNames, pick.RandomEveryTime, time.Hour)
|
||||
|
||||
case "":
|
||||
log.Info("The first template (ordered by name) will be used")
|
||||
|
||||
@ -132,6 +142,12 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config
|
||||
case <-ctx.Done(): // ..or context cancellation
|
||||
log.Info("Gracefully server stopping", zap.Duration("uptime", time.Since(startedAt)))
|
||||
|
||||
if p, ok := picker.(interface{ Close() error }); ok {
|
||||
if err := p.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// stop the server using created context above
|
||||
if err := server.Stop(); err != nil {
|
||||
return err
|
||||
|
@ -35,6 +35,8 @@ const (
|
||||
const (
|
||||
useRandomTemplate = "random"
|
||||
useRandomTemplateOnEachRequest = "i-said-random"
|
||||
useRandomTemplateDaily = "random-daily"
|
||||
useRandomTemplateHourly = "random-hourly"
|
||||
)
|
||||
|
||||
func (f *flags) init(flagSet *pflag.FlagSet) {
|
||||
@ -55,8 +57,13 @@ func (f *flags) init(flagSet *pflag.FlagSet) {
|
||||
templateNameFlagName, "t",
|
||||
"",
|
||||
fmt.Sprintf(
|
||||
"template name (set \"%s\" to use a randomized or \"%s\" to use a randomized template on each request) [$%s]", //nolint:lll
|
||||
useRandomTemplate, useRandomTemplateOnEachRequest, env.TemplateName,
|
||||
"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(
|
||||
|
@ -51,33 +51,38 @@ func (p *picker) NextIndex() uint32 {
|
||||
|
||||
case RandomOnce:
|
||||
if p.lastIdx == unsetIdx {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
p.lastIdx = uint32(p.rand.Intn(int(p.maxIdx)))
|
||||
return p.randomizeNext()
|
||||
}
|
||||
|
||||
return p.lastIdx
|
||||
|
||||
case RandomEveryTime:
|
||||
var idx = uint32(p.rand.Intn(int(p.maxIdx + 1)))
|
||||
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if idx == p.lastIdx {
|
||||
p.lastIdx++
|
||||
} else {
|
||||
p.lastIdx = idx
|
||||
}
|
||||
|
||||
if p.lastIdx > p.maxIdx { // overflow?
|
||||
p.lastIdx = 0
|
||||
}
|
||||
|
||||
return p.lastIdx
|
||||
return p.randomizeNext()
|
||||
|
||||
default:
|
||||
panic("picker.NextIndex(): unsupported mode")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *picker) randomizeNext() uint32 {
|
||||
var idx = uint32(p.rand.Intn(int(p.maxIdx + 1)))
|
||||
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if idx == p.lastIdx {
|
||||
p.lastIdx++
|
||||
} else {
|
||||
p.lastIdx = idx
|
||||
}
|
||||
|
||||
if p.lastIdx > p.maxIdx { // overflow?
|
||||
p.lastIdx = 0
|
||||
}
|
||||
|
||||
if p.lastIdx == unsetIdx {
|
||||
p.lastIdx--
|
||||
}
|
||||
|
||||
return p.lastIdx
|
||||
}
|
||||
|
@ -1,5 +1,11 @@
|
||||
package pick
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type StringsSlice struct {
|
||||
s []string
|
||||
p *picker
|
||||
@ -7,7 +13,13 @@ type StringsSlice struct {
|
||||
|
||||
// NewStringsSlice creates new StringsSlice.
|
||||
func NewStringsSlice(items []string, mode pickMode) *StringsSlice {
|
||||
return &StringsSlice{s: items, p: NewPicker(uint32(len(items)-1), mode)}
|
||||
maxIdx := len(items) - 1
|
||||
|
||||
if maxIdx < 0 {
|
||||
maxIdx = 0
|
||||
}
|
||||
|
||||
return &StringsSlice{s: items, p: NewPicker(uint32(maxIdx), mode)}
|
||||
}
|
||||
|
||||
// Pick an element from the strings slice.
|
||||
@ -18,3 +30,106 @@ func (s *StringsSlice) Pick() string {
|
||||
|
||||
return s.s[s.p.NextIndex()]
|
||||
}
|
||||
|
||||
type StringsSliceWithInterval struct {
|
||||
s []string
|
||||
p *picker
|
||||
d time.Duration
|
||||
|
||||
idxMu sync.RWMutex
|
||||
idx uint32
|
||||
|
||||
close chan struct{}
|
||||
closedMu sync.RWMutex
|
||||
closed bool
|
||||
}
|
||||
|
||||
// NewStringsSliceWithInterval creates new StringsSliceWithInterval.
|
||||
func NewStringsSliceWithInterval(items []string, mode pickMode, interval time.Duration) *StringsSliceWithInterval {
|
||||
maxIdx := len(items) - 1
|
||||
|
||||
if maxIdx < 0 {
|
||||
maxIdx = 0
|
||||
}
|
||||
|
||||
if interval <= time.Duration(0) {
|
||||
panic("NewStringsSliceWithInterval: wrong interval")
|
||||
}
|
||||
|
||||
s := &StringsSliceWithInterval{
|
||||
s: items,
|
||||
p: NewPicker(uint32(maxIdx), mode),
|
||||
d: interval,
|
||||
close: make(chan struct{}, 1),
|
||||
}
|
||||
|
||||
s.next()
|
||||
|
||||
go s.rotate()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StringsSliceWithInterval) rotate() {
|
||||
defer close(s.close)
|
||||
|
||||
timer := time.NewTimer(s.d)
|
||||
defer timer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.close:
|
||||
return
|
||||
|
||||
case <-timer.C:
|
||||
s.next()
|
||||
timer.Reset(s.d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StringsSliceWithInterval) next() {
|
||||
idx := s.p.NextIndex()
|
||||
|
||||
s.idxMu.Lock()
|
||||
s.idx = idx
|
||||
s.idxMu.Unlock()
|
||||
}
|
||||
|
||||
// Pick an element from the strings slice.
|
||||
func (s *StringsSliceWithInterval) Pick() string {
|
||||
if s.isClosed() {
|
||||
panic("StringsSliceWithInterval.Pick(): closed")
|
||||
}
|
||||
|
||||
if len(s.s) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
s.idxMu.RLock()
|
||||
defer s.idxMu.RUnlock()
|
||||
|
||||
return s.s[s.idx]
|
||||
}
|
||||
|
||||
func (s *StringsSliceWithInterval) isClosed() (closed bool) {
|
||||
s.closedMu.RLock()
|
||||
closed = s.closed
|
||||
s.closedMu.RUnlock()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *StringsSliceWithInterval) Close() error {
|
||||
if s.isClosed() {
|
||||
return errors.New("closed")
|
||||
}
|
||||
|
||||
s.closedMu.Lock()
|
||||
s.closed = true
|
||||
s.closedMu.Unlock()
|
||||
|
||||
s.close <- struct{}{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package pick_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/pick"
|
||||
@ -47,3 +48,83 @@ func TestStringsSlice_Pick(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewStringsSliceWithInterval_Pick(t *testing.T) {
|
||||
t.Run("first", func(t *testing.T) {
|
||||
for i := uint8(0); i < 50; i++ {
|
||||
p := pick.NewStringsSliceWithInterval([]string{}, pick.First, time.Millisecond)
|
||||
assert.Equal(t, "", p.Pick())
|
||||
assert.NoError(t, p.Close())
|
||||
assert.Panics(t, func() { p.Pick() })
|
||||
}
|
||||
|
||||
p := pick.NewStringsSliceWithInterval([]string{"foo", "bar", "baz"}, pick.First, time.Millisecond)
|
||||
|
||||
for i := uint8(0); i < 50; i++ {
|
||||
assert.Equal(t, "foo", p.Pick())
|
||||
|
||||
<-time.After(time.Millisecond * 2)
|
||||
}
|
||||
|
||||
assert.NoError(t, p.Close())
|
||||
assert.Error(t, p.Close())
|
||||
assert.Panics(t, func() { p.Pick() })
|
||||
})
|
||||
|
||||
t.Run("random once", func(t *testing.T) {
|
||||
for i := uint8(0); i < 50; i++ {
|
||||
p := pick.NewStringsSliceWithInterval([]string{}, pick.RandomOnce, time.Millisecond)
|
||||
assert.Equal(t, "", p.Pick())
|
||||
assert.NoError(t, p.Close())
|
||||
assert.Panics(t, func() { p.Pick() })
|
||||
}
|
||||
|
||||
var (
|
||||
p = pick.NewStringsSliceWithInterval([]string{"foo", "bar", "baz"}, pick.RandomOnce, time.Millisecond)
|
||||
picked = p.Pick()
|
||||
)
|
||||
|
||||
for i := uint8(0); i < 50; i++ {
|
||||
assert.Equal(t, picked, p.Pick())
|
||||
|
||||
<-time.After(time.Millisecond * 2)
|
||||
}
|
||||
|
||||
assert.NoError(t, p.Close())
|
||||
assert.Error(t, p.Close())
|
||||
assert.Panics(t, func() { p.Pick() })
|
||||
})
|
||||
|
||||
t.Run("random every time", func(t *testing.T) {
|
||||
for i := uint8(0); i < 50; i++ {
|
||||
p := pick.NewStringsSliceWithInterval([]string{}, pick.RandomEveryTime, time.Millisecond)
|
||||
assert.Equal(t, "", p.Pick())
|
||||
assert.NoError(t, p.Close())
|
||||
assert.Panics(t, func() { p.Pick() })
|
||||
}
|
||||
|
||||
var changed int
|
||||
|
||||
for i := uint8(0); i < 50; i++ {
|
||||
p := pick.NewStringsSliceWithInterval([]string{"foo", "bar", "baz"}, pick.RandomEveryTime, time.Millisecond) //nolint:lll
|
||||
|
||||
one, two := p.Pick(), p.Pick()
|
||||
assert.Equal(t, one, two)
|
||||
|
||||
<-time.After(time.Millisecond * 2)
|
||||
|
||||
three, four := p.Pick(), p.Pick()
|
||||
assert.Equal(t, three, four)
|
||||
|
||||
if one != three {
|
||||
changed++
|
||||
}
|
||||
|
||||
assert.NoError(t, p.Close())
|
||||
assert.Error(t, p.Close())
|
||||
assert.Panics(t, func() { p.Pick() })
|
||||
}
|
||||
|
||||
assert.GreaterOrEqual(t, changed, 25)
|
||||
})
|
||||
}
|
||||
|
@ -1 +1,37 @@
|
||||
# TODO: Write docs
|
||||
# Templates
|
||||
|
||||
Creating templates is a very simple operation, even for those who know nothing at all about [Go Template](https://pkg.go.dev/text/template). All you should know is:
|
||||
|
||||
- The template should be one page. Without additional `css` or `js` files (but you can load them from the CDN or another GitHub repositories using [jsdelivr.com](https://www.jsdelivr.com/), for example)
|
||||
- Don't forget to include `<meta name="robots" content="noindex, nofollow" />` tag in the header
|
||||
- You can use a special "placeholders" (wrapped in `{{` and `}}`) for the rendering error code, message and others (see details below)
|
||||
|
||||
## Supported signatures
|
||||
|
||||
### Error page & request data
|
||||
|
||||
| Signature | Description | Example |
|
||||
|-------------------------------------|---------------------------------------------------------------|----------------------------------------------|
|
||||
| `{{ code }}` | Error page code | `404` |
|
||||
| `{{ message }}` | Error code message | `Not found` |
|
||||
| `{{ description }}` | Error code description | `The server can not find the requested page` |
|
||||
| `{{ original_uri }}` | `X-Original-URI` header value | `/foo1/bar2` |
|
||||
| `{{ namespace }}` | `X-Namespace` header value | `foo` |
|
||||
| `{{ ingress_name }}` | `X-Ingress-Name` header value | `bar` |
|
||||
| `{{ service_name }}` | `X-Service-Name` header value | `baz` |
|
||||
| `{{ service_port }}` | `X-Service-Port` header value | `8080` |
|
||||
| `{{ request_id }}` | `X-Request-ID` header value | `12AB34CD56EF78` |
|
||||
| `{{ forwarded_for }}` | `X-Forwarded-For` header value | `203.0.113.195, 70.41.3.18` |
|
||||
| `{{ host }}` | `Host` header value | `example.com` |
|
||||
| `{{ now.Unix }}` | Current timestamp (e.g. in Unix format) | `1643621927` |
|
||||
| `{{ hostname }}` | OS hostname | `ab12cd34ef56` |
|
||||
| `{{ version }}` | Application version | `2.5.0` |
|
||||
| `{{ if show_details }}...{{ end }}` | Logical operator (server started with "show details" option?) | |
|
||||
| `{{ if hide_details }}...{{ end }}` | Same as above, but inverted | |
|
||||
|
||||
### Modifiers
|
||||
|
||||
| Signature | Description | Example |
|
||||
|------------------------------------|--------------------------------|-------------------------------------|
|
||||
| <code>{{ ... | json }}</code> | Convert value into json-string | <code>{{ code | json }}</code> |
|
||||
| <code>{{ ... | int }}</code> | Convert value into integer | <code>{{ code | int }}</code> |
|
||||
|
Reference in New Issue
Block a user