mirror of
https://github.com/tarampampam/error-pages.git
synced 2024-08-30 18:22:40 +00:00
feat: templates caching
This commit is contained in:
parent
075bc8f57c
commit
5698daa36c
@ -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
|
||||
|
111
internal/http/handlers/error_page/cache.go
Normal file
111
internal/http/handlers/error_page/cache.go
Normal 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
|
||||
}
|
86
internal/http/handlers/error_page/cache_test.go
Normal file
86
internal/http/handlers/error_page/cache_test.go
Normal 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
|
||||
}
|
@ -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 (
|
||||
|
@ -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)
|
||||
|
@ -23,8 +23,9 @@ 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.
|
||||
@ -45,21 +46,26 @@ func NewServer(log *logger.Logger, readBufferSize uint) Server {
|
||||
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())
|
||||
|
||||
@ -134,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)
|
||||
}
|
||||
|
@ -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>`))
|
||||
|
||||
|
@ -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`
|
||||
|
@ -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},
|
||||
|
@ -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 -}} -->
|
||||
|
@ -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>
|
||||
|
@ -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 -}} -->
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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 -}} -->
|
||||
|
Loading…
Reference in New Issue
Block a user