Compare commits

..

10 Commits

Author SHA1 Message Date
746ce05ae7 add static html pages into releases 2024-07-02 23:57:47 +04:00
5698daa36c feat: templates caching 2024-07-02 23:42:09 +04:00
075bc8f57c chore: move --read-buffer-size back 2024-07-02 15:01:56 +04:00
5a2e678d33 Update README.md (#291)
Add note that the cat template is the only one that fetches external resources.

Closes: #274
2024-07-02 13:16:37 +04:00
ae73819644 go mod tidy 2024-07-02 11:51:54 +04:00
ed427d614f Merge branch 'master' into v3 2024-07-02 00:51:28 -07:00
0f221d4016 docker/build-push-action action updated 2024-07-02 11:48:49 +04:00
d5a51e22e2 the prototype is done 2024-07-02 11:47:35 +04:00
d4b2b5ef96 Merge pull request #290 from tarampampam/dependabot/go_modules/gomod-bdfb6ece4e
build(deps): bump github.com/valyala/fasthttp from 1.54.0 to 1.55.0 in the gomod group
2024-07-01 22:58:34 +00:00
f7bbaf97f0 build(deps): bump github.com/valyala/fasthttp in the gomod group
Bumps the gomod group with 1 update: [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp).


Updates `github.com/valyala/fasthttp` from 1.54.0 to 1.55.0
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/1.54.0...v1.55.0)

---
updated-dependencies:
- dependency-name: github.com/valyala/fasthttp
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 22:58:20 +00:00
24 changed files with 353 additions and 70 deletions

View File

@ -44,6 +44,16 @@ jobs:
path: out/
if-no-files-found: error
retention-days: 1
- if: matrix.os == 'linux' && matrix.arch == 'amd64'
working-directory: ./out
run: zip -r ./../templates.zip .
- if: matrix.os == 'linux' && matrix.arch == 'amd64'
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: templates.zip
asset_name: error-pages-static.zip
tag: ${{ github.ref }}
demo:
name: Update the demo (GitHub Pages)
@ -76,7 +86,7 @@ jobs:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
- uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile

View File

@ -83,7 +83,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- {uses: gacts/github-slug@v1, id: slug}
- uses: docker/build-push-action@v5
- uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile

View File

@ -44,6 +44,7 @@ The following flags are supported:
| `--show-details` | show request details in the error page response (if supported by the template) | `false` | `SHOW_DETAILS` |
| `--proxy-headers="…"` | HTTP headers listed here will be proxied from the original request to the error page response (comma-separated list) | `X-Request-Id,X-Trace-Id,X-Amzn-Trace-Id` | `PROXY_HTTP_HEADERS` |
| `--rotation-mode="…"` | templates automatic rotation mode (disabled/random-on-startup/random-on-each-request/random-hourly/random-daily) | `disabled` | `TEMPLATES_ROTATION_MODE` |
| `--read-buffer-size="…"` | per-connection buffer size in bytes for reading requests, this also limits the maximum header size (increase this buffer if your clients send multi-KB Request URIs and/or multi-KB headers (e.g., large cookies), note that increasing this value will increase memory consumption) | `5120` | `READ_BUFFER_SIZE` |
### `build` command (aliases: `b`)
@ -88,6 +89,10 @@ The following flags are supported:
The following templates are built-in and available for use without any additional setup:
> [!NOTE]
> The `cat` template is the only one of those that fetches resources (the actual cat pictures)
> from external servers - all other templates are self-contained.
<table>
<thead>
<tr>

2
go.mod
View File

@ -6,6 +6,7 @@ require (
github.com/stretchr/testify v1.9.0
github.com/urfave/cli-docs/v3 v3.0.0-alpha5
github.com/urfave/cli/v3 v3.0.0-alpha9
github.com/valyala/fasthttp v1.55.0
go.uber.org/automaxprocs v1.5.3
)
@ -19,7 +20,6 @@ require (
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.55.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

View File

@ -22,9 +22,9 @@ type command struct {
opt struct {
http struct { // our HTTP server
addr string
port uint16
// readBufferSize uint
addr string
port uint16
readBufferSize uint
}
}
}
@ -140,6 +140,15 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
return nil
},
}
readBufferSizeFlag = cli.UintFlag{
Name: "read-buffer-size",
Usage: "per-connection buffer size in bytes for reading requests, this also limits the maximum header size " +
"(increase this buffer if your clients send multi-KB Request URIs and/or multi-KB headers (e.g., " +
"large cookies), note that increasing this value will increase memory consumption)",
Value: 1024 * 5, //nolint:mnd // 5 KB
Sources: env("READ_BUFFER_SIZE"),
OnlyOnce: true,
}
)
// override some flag usage messages
@ -157,6 +166,7 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
Action: func(ctx context.Context, c *cli.Command) error {
cmd.opt.http.addr = c.String(addrFlag.Name)
cmd.opt.http.port = uint16(c.Uint(portFlag.Name))
cmd.opt.http.readBufferSize = uint(c.Uint(readBufferSizeFlag.Name))
cfg.L10n.Disable = c.Bool(disableL10nFlag.Name)
cfg.DefaultCodeToRender = uint16(c.Uint(defaultCodeToRenderFlag.Name))
cfg.RespondWithSameHTTPCode = c.Bool(sendSameHTTPCodeFlag.Name)
@ -282,6 +292,7 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
&showDetailsFlag,
&proxyHeadersListFlag,
&rotationModeFlag,
&readBufferSizeFlag,
},
}
@ -290,7 +301,7 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
// Run current command.
func (cmd *command) Run(ctx context.Context, log *logger.Logger, cfg *config.Config) error { //nolint:funlen
var srv = appHttp.NewServer(log)
var srv = appHttp.NewServer(log, cmd.opt.http.readBufferSize)
if err := srv.Register(cfg); err != nil {
return err

View File

@ -72,7 +72,7 @@ const defaultJSONFormat string = `{
"service_name": {{ service_name | json }},
"service_port": {{ service_port | json }},
"request_id": {{ request_id | json }},
"timestamp": {{ now.Unix }}
"timestamp": {{ nowUnix }}
}{{ end }}
}
` // an empty line at the end is important for better UX
@ -91,7 +91,7 @@ const defaultXMLFormat string = `<?xml version="1.0" encoding="utf-8"?>
<serviceName>{{ service_name }}</serviceName>
<servicePort>{{ service_port }}</servicePort>
<requestID>{{ request_id }}</requestID>
<timestamp>{{ now.Unix }}</timestamp>
<timestamp>{{ nowUnix }}</timestamp>
</details>{{ end }}
</error>
` // an empty line at the end is important for better UX
@ -107,7 +107,7 @@ Ingress Name: {{ ingress_name }}
Service Name: {{ service_name }}
Service Port: {{ service_port }}
Request ID: {{ request_id }}
Timestamp: {{ now.Unix }}{{ end }}
Timestamp: {{ nowUnix }}{{ end }}
` // an empty line at the end is important for better UX
//nolint:lll

View File

@ -0,0 +1,111 @@
package error_page
import (
"bytes"
"crypto/md5" //nolint:gosec
"encoding/gob"
"sync"
"time"
"gh.tarampamp.am/error-pages/internal/template"
)
type (
// RenderedCache is a cache for rendered error pages. It's safe for concurrent use.
// It uses a hash of the template and props as a key.
//
// To remove expired items, call ClearExpired method periodically (a bit more often than the ttl).
RenderedCache struct {
ttl time.Duration
mu sync.RWMutex
items map[[32]byte]cacheItem // map[template_hash[0:15];props_hash[16:32]]cache_item
}
cacheItem struct {
content []byte
addedAtNano int64
}
)
// NewRenderedCache creates a new RenderedCache with the specified ttl.
func NewRenderedCache(ttl time.Duration) *RenderedCache {
return &RenderedCache{ttl: ttl, items: make(map[[32]byte]cacheItem)}
}
// genKey generates a key for the cache item by hashing the template and props.
func (rc *RenderedCache) genKey(template string, props template.Props) [32]byte {
var (
key [32]byte
th, ph = hash(template), hash(props) // template hash, props hash
)
copy(key[:16], th[:]) // first 16 bytes for the template hash
copy(key[16:], ph[:]) // last 16 bytes for the props hash
return key
}
// Has checks if the cache has an item with the specified template and props.
func (rc *RenderedCache) Has(template string, props template.Props) bool {
var key = rc.genKey(template, props)
rc.mu.RLock()
_, ok := rc.items[key]
rc.mu.RUnlock()
return ok
}
// Put adds a new item to the cache with the specified template, props, and content.
func (rc *RenderedCache) Put(template string, props template.Props, content []byte) {
var key = rc.genKey(template, props)
rc.mu.Lock()
rc.items[key] = cacheItem{content: content, addedAtNano: time.Now().UnixNano()}
rc.mu.Unlock()
}
// Get returns the content of the item with the specified template and props.
func (rc *RenderedCache) Get(template string, props template.Props) ([]byte, bool) {
var key = rc.genKey(template, props)
rc.mu.RLock()
item, ok := rc.items[key]
rc.mu.RUnlock()
return item.content, ok
}
// ClearExpired removes all expired items from the cache.
func (rc *RenderedCache) ClearExpired() {
rc.mu.Lock()
var now = time.Now().UnixNano()
for key, item := range rc.items {
if now-item.addedAtNano > rc.ttl.Nanoseconds() {
delete(rc.items, key)
}
}
rc.mu.Unlock()
}
// Clear removes all items from the cache.
func (rc *RenderedCache) Clear() {
rc.mu.Lock()
clear(rc.items)
rc.mu.Unlock()
}
// hash returns an MD5 hash of the provided value (it may be any built-in type).
func hash(in any) [16]byte {
var b bytes.Buffer
if err := gob.NewEncoder(&b).Encode(in); err != nil {
return [16]byte{} // never happens because we encode only built-in types
}
return md5.Sum(b.Bytes()) //nolint:gosec
}

View File

@ -0,0 +1,86 @@
package error_page_test
import (
"strconv"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/http/handlers/error_page"
"gh.tarampamp.am/error-pages/internal/template"
)
func TestRenderedCache_CRUD(t *testing.T) {
t.Parallel()
var cache = error_page.NewRenderedCache(time.Millisecond)
t.Run("has", func(t *testing.T) {
assert.False(t, cache.Has("template", template.Props{}))
cache.Put("template", template.Props{}, []byte("content"))
assert.True(t, cache.Has("template", template.Props{}))
assert.False(t, cache.Has("template", template.Props{Code: 1}))
assert.False(t, cache.Has("foo", template.Props{Code: 1}))
})
t.Run("exists", func(t *testing.T) {
var got, ok = cache.Get("template", template.Props{})
assert.True(t, ok)
assert.Equal(t, []byte("content"), got)
cache.Clear()
assert.False(t, cache.Has("template", template.Props{}))
})
t.Run("not exists", func(t *testing.T) {
var got, ok = cache.Get("template", template.Props{Code: 2})
assert.False(t, ok)
assert.Nil(t, got)
})
t.Run("race condition provocation", func(t *testing.T) {
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(2)
go func(i int) {
defer wg.Done()
cache.Get("template", template.Props{})
cache.Put("template"+strconv.Itoa(i), template.Props{}, []byte("content"))
cache.Has("template", template.Props{})
}(i)
go func() {
defer wg.Done()
cache.ClearExpired()
}()
}
wg.Wait()
})
}
func TestRenderedCache_Expiring(t *testing.T) {
t.Parallel()
var cache = error_page.NewRenderedCache(10 * time.Millisecond)
cache.Put("template", template.Props{}, []byte("content"))
cache.ClearExpired()
assert.True(t, cache.Has("template", template.Props{}))
<-time.After(10 * time.Millisecond)
assert.True(t, cache.Has("template", template.Props{})) // expired, but not cleared yet
cache.ClearExpired()
assert.False(t, cache.Has("template", template.Props{})) // cleared
}

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"sync"
"sync/atomic"
"time"
@ -15,7 +16,32 @@ import (
)
// New creates a new handler that returns an error page with the specified status code and format.
func New(cfg *config.Config, log *logger.Logger) fasthttp.RequestHandler { //nolint:funlen,gocognit,gocyclo
func New(cfg *config.Config, log *logger.Logger) (_ fasthttp.RequestHandler, closeCache func()) { //nolint:funlen,gocognit,gocyclo,lll
// if the ttl will be bigger than 1 second, the template functions like `nowUnix` will not work as expected
const cacheTtl = 900 * time.Millisecond // the cache TTL
var (
cache, stopCh = NewRenderedCache(cacheTtl), make(chan struct{})
stopOnce sync.Once
)
// run a goroutine that will clear the cache from expired items. to stop the goroutine - close the stop channel
// or call the closeCache
go func() {
var timer = time.NewTimer(cacheTtl)
defer func() { timer.Stop(); cache.Clear() }()
for {
select {
case <-timer.C:
cache.ClearExpired()
timer.Reset(cacheTtl)
case <-stopCh:
return
}
}
}()
return func(ctx *fasthttp.RequestCtx) {
var (
reqHeaders = &ctx.Request.Header
@ -106,57 +132,82 @@ func New(cfg *config.Config, log *logger.Logger) fasthttp.RequestHandler { //nol
switch {
case format == jsonFormat && cfg.Formats.JSON != "":
if content, err := template.Render(cfg.Formats.JSON, tplProps); err != nil {
j, _ := json.Marshal(fmt.Sprintf("Failed to render the JSON template: %s", err.Error()))
write(ctx, log, j)
} else {
write(ctx, log, content)
if cached, ok := cache.Get(cfg.Formats.JSON, tplProps); ok { // cache hit
write(ctx, log, cached)
} else { // cache miss
if content, err := template.Render(cfg.Formats.JSON, tplProps); err != nil {
errAsJson, _ := json.Marshal(fmt.Sprintf("Failed to render the JSON template: %s", err.Error()))
write(ctx, log, errAsJson) // error during rendering
} else {
cache.Put(cfg.Formats.JSON, tplProps, []byte(content))
write(ctx, log, content) // rendered successfully
}
}
case format == xmlFormat && cfg.Formats.XML != "":
if content, err := template.Render(cfg.Formats.XML, tplProps); err != nil {
write(ctx, log, fmt.Sprintf(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<error>Failed to render the XML template: %s</error>", err.Error(),
))
} else {
write(ctx, log, content)
if cached, ok := cache.Get(cfg.Formats.XML, tplProps); ok { // cache hit
write(ctx, log, cached)
} else { // cache miss
if content, err := template.Render(cfg.Formats.XML, tplProps); err != nil {
write(ctx, log, fmt.Sprintf(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<error>Failed to render the XML template: %s</error>\n", err.Error(),
))
} else {
cache.Put(cfg.Formats.XML, tplProps, []byte(content))
write(ctx, log, content)
}
}
case format == htmlFormat:
var templateName = templateToUse(cfg)
if tpl, found := cfg.Templates.Get(templateName); found {
if content, err := template.Render(tpl, tplProps); err != nil {
// TODO: add GZIP compression for the HTML content support
write(ctx, log, fmt.Sprintf(
"<!DOCTYPE html>\n<html><body>Failed to render the HTML template %s: %s</body></html>",
templateName,
err.Error(),
))
} else {
write(ctx, log, content)
if tpl, found := cfg.Templates.Get(templateName); found { //nolint:nestif
if cached, ok := cache.Get(tpl, tplProps); ok { // cache hit
write(ctx, log, cached)
} else { // cache miss
if content, err := template.Render(tpl, tplProps); err != nil {
// TODO: add GZIP compression for the HTML content support
write(ctx, log, fmt.Sprintf(
"<!DOCTYPE html>\n<html><body>Failed to render the HTML template %s: %s</body></html>\n",
templateName,
err.Error(),
))
} else {
cache.Put(tpl, tplProps, []byte(content))
write(ctx, log, content)
}
}
} else {
write(ctx, log, fmt.Sprintf(
"<!DOCTYPE html>\n<html><body>Template %s not found and cannot be used</body></html>", templateName,
"<!DOCTYPE html>\n<html><body>Template %s not found and cannot be used</body></html>\n", templateName,
))
}
default: // plainTextFormat as default
if cfg.Formats.PlainText != "" {
if content, err := template.Render(cfg.Formats.PlainText, tplProps); err != nil {
write(ctx, log, fmt.Sprintf("Failed to render the PlainText template: %s", err.Error()))
} else {
write(ctx, log, content)
if cfg.Formats.PlainText != "" { //nolint:nestif
if cached, ok := cache.Get(cfg.Formats.PlainText, tplProps); ok { // cache hit
write(ctx, log, cached)
} else { // cache miss
if content, err := template.Render(cfg.Formats.PlainText, tplProps); err != nil {
write(ctx, log, fmt.Sprintf("Failed to render the PlainText template: %s", err.Error()))
} else {
cache.Put(cfg.Formats.PlainText, tplProps, []byte(content))
write(ctx, log, content)
}
}
} else {
write(ctx, log, `The requested content format is not supported.
Please create an issue on the project's GitHub page to request support for this format.
Supported formats: JSON, XML, HTML, Plain Text`)
Supported formats: JSON, XML, HTML, Plain Text
`)
}
}
}
}, func() { stopOnce.Do(func() { close(stopCh) }) }
}
var (

View File

@ -159,7 +159,8 @@ func TestHandler(t *testing.T) {
t.Run(name, func(t *testing.T) {
t.Parallel()
var handler = error_page.New(tt.giveConfig(), logger.NewNop())
var handler, closeCache = error_page.New(tt.giveConfig(), logger.NewNop())
defer closeCache()
req, reqErr := http.NewRequest(http.MethodGet, tt.giveUrl, http.NoBody)
require.NoError(t, reqErr)
@ -202,9 +203,11 @@ func TestRotationModeOnEachRequest(t *testing.T) {
lastResponseBody string
changedTimes int
handler = error_page.New(&cfg, logger.NewNop())
handler, closeCache = error_page.New(&cfg, logger.NewNop())
)
defer func() { closeCache(); closeCache(); closeCache() }() // multiple calls should not panic
for range 300 {
req, reqErr := http.NewRequest(http.MethodGet, "http://testing/", http.NoBody)
require.NoError(t, reqErr)

View File

@ -23,16 +23,16 @@ import (
// Server is an HTTP server for serving error pages.
type Server struct {
log *logger.Logger
server *fasthttp.Server
log *logger.Logger
server *fasthttp.Server
beforeStop func()
}
// NewServer creates a new HTTP server.
func NewServer(log *logger.Logger) Server {
func NewServer(log *logger.Logger, readBufferSize uint) Server {
const (
readTimeout = 30 * time.Second
writeTimeout = readTimeout + 10*time.Second // should be bigger than the read timeout
maxHeaderBytes = (1 << 20) * 5 //nolint:mnd // 5 MB
readTimeout = 30 * time.Second
writeTimeout = readTimeout + 10*time.Second // should be bigger than the read timeout
)
return Server{
@ -40,27 +40,32 @@ func NewServer(log *logger.Logger) Server {
server: &fasthttp.Server{
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
ReadBufferSize: maxHeaderBytes,
ReadBufferSize: int(readBufferSize),
DisablePreParseMultipartForm: true,
NoDefaultServerHeader: true,
CloseOnShutdown: true,
Logger: logger.NewStdLog(log),
},
beforeStop: func() {}, // noop
}
}
// Register server handlers, middlewares, etc.
func (s *Server) Register(cfg *config.Config) error {
var (
liveHandler = live.New()
versionHandler = version.New(appmeta.Version())
errorPagesHandler = ep.New(cfg, s.log)
faviconHandler = static.New(static.Favicon)
liveHandler = live.New()
versionHandler = version.New(appmeta.Version())
faviconHandler = static.New(static.Favicon)
errorPagesHandler, closeCache = ep.New(cfg, s.log)
notFound = http.StatusText(http.StatusNotFound) + "\n"
notAllowed = http.StatusText(http.StatusMethodNotAllowed) + "\n"
)
// wrap the before shutdown function to close the cache
s.beforeStop = closeCache
s.server.Handler = func(ctx *fasthttp.RequestCtx) {
var url, method = string(ctx.Path()), string(ctx.Method())
@ -135,5 +140,7 @@ func (s *Server) Stop(timeout time.Duration) error {
var ctx, cancel = context.WithTimeout(context.Background(), timeout)
defer cancel()
s.beforeStop()
return s.server.ShutdownWithContext(ctx)
}

View File

@ -20,7 +20,7 @@ import (
// TestRouting in fact is a test for the whole server, because it tests all the routes and their handlers.
func TestRouting(t *testing.T) {
var (
srv = appHttp.NewServer(logger.NewNop())
srv = appHttp.NewServer(logger.NewNop(), 1025*5)
cfg = config.New()
)
@ -38,7 +38,7 @@ func TestRouting(t *testing.T) {
Service Name: {{ service_name }}
Service Port: {{ service_port }}
Request ID: {{ request_id }}
Timestamp: {{ now.Unix }}
Timestamp: {{ nowUnix }}
</pre>{{ end }}
</html>`))

View File

@ -16,10 +16,9 @@ import (
)
var builtInFunctions = template.FuncMap{ //nolint:gochecknoglobals
// current time:
// `{{ now.Unix }}` // `1631610000`
// `{{ now.Hour }}:{{ now.Minute }}:{{ now.Second }}` // `15:4:5`
"now": time.Now,
// the current time in unix format (seconds since 1970 UTC):
// `{{ nowUnix }}` // `1631610000`
"nowUnix": func() int64 { return time.Now().Unix() },
// current hostname:
// `{{ hostname }}` // `localhost`

View File

@ -27,7 +27,7 @@ func TestRender_BuiltInFunction(t *testing.T) {
wantErrMsg string
}{
"now (unix)": {
giveTemplate: `{{ now.Unix }}`,
giveTemplate: `{{ nowUnix }}`,
wantResult: strconv.Itoa(int(time.Now().Unix())),
},
"hostname": {giveTemplate: `{{ hostname }}`, wantResult: hostname},

View File

@ -341,7 +341,7 @@
<!-- {{- end }}{{ if request_id -}} -->
<li><span data-l10n>Request ID</span>: <code>{{ request_id }}</code></li>
<!-- {{- end -}} -->
<li><span data-l10n>Timestamp</span>: <code>{{ now.Unix }}</code></li>
<li><span data-l10n>Timestamp</span>: <code>{{ nowUnix }}</code></li>
</ul>
</div>
<!-- {{- end -}} -->

View File

@ -150,7 +150,7 @@
<!-- {{- end -}} -->
<tr>
<td class="name" data-l10n>Timestamp</td>
<td class="value">{{ now.Unix }}</td>
<td class="value">{{ nowUnix }}</td>
</tr>
</tbody>
</table>

View File

@ -327,7 +327,7 @@
<!-- {{- end }}{{ if request_id -}} -->
<li><span data-l10n>Request ID</span>: <code>{{ request_id }}</code></li>
<!-- {{- end -}} -->
<li><span data-l10n>Timestamp</span>: <code>{{ now.Unix }}</code></li>
<li><span data-l10n>Timestamp</span>: <code>{{ nowUnix }}</code></li>
</ul>
</div>
<!-- {{- end -}} -->

View File

@ -235,7 +235,7 @@
<!-- {{- end -}} -->
<tr>
<td class="name" data-l10n>Timestamp</td>
<td class="value">{{ now.Unix }}</td>
<td class="value">{{ nowUnix }}</td>
</tr>
</tbody>
</table>

View File

@ -174,7 +174,7 @@
<!-- {{- end }}{{ if request_id -}} -->
<p class="output small"><span data-l10n>Request ID</span>: <code>{{ request_id }}</code></p>
<!-- {{- end -}} -->
<p class="output small"><span data-l10n>Timestamp</span>: <code>{{ now.Unix }}</code></p>
<p class="output small"><span data-l10n>Timestamp</span>: <code>{{ nowUnix }}</code></p>
</div>
<!-- {{- end -}} -->
</main>

View File

@ -157,7 +157,7 @@
<!-- {{- end }}{{ if request_id -}} -->
<li class="value">{{ request_id }}</li>
<!-- {{- end -}} -->
<li class="value">{{ now.Unix }}</li>
<li class="value">{{ nowUnix }}</li>
</ul>
<!-- {{- end -}} -->
</div>

View File

@ -442,7 +442,7 @@
<!-- {{- end }}{{ if request_id -}} -->
<li><span data-l10n>Request ID</span>: <code>{{ request_id }}</code></li>
<!-- {{- end -}} -->
<li><span data-l10n>Timestamp</span>: <code>{{ now.Unix }}</code></li>
<li><span data-l10n>Timestamp</span>: <code>{{ nowUnix }}</code></li>
</ul>
<!-- {{- end -}} -->
</div>

View File

@ -9,7 +9,7 @@
{{ if service_name }}Service name: {{ service_name }}{{ end }}
{{ if service_port }}Service port: {{ service_port }}{{ end }}
{{ if request_id }}Request ID: {{ request_id }}{{ end }}
Timestamp: {{ now.Unix }}
Timestamp: {{ nowUnix }}
{{ end }}
-->
<html lang="en">

View File

@ -253,7 +253,7 @@
<!-- {{- end -}} -->
<tr>
<td class="name" data-l10n>Timestamp</td>
<td class="value">{{ now.Unix }}</td>
<td class="value">{{ nowUnix }}</td>
</tr>
</table>
</div>

View File

@ -167,7 +167,7 @@
<!-- {{- end -}} -->
<tr>
<td class="name"><span data-l10n>Timestamp</span>:</td>
<td class="value">{{ now.Unix }}</td>
<td class="value">{{ nowUnix }}</td>
</tr>
</table>
<!-- {{- end -}} -->