Compare commits

..

1 Commits

Author SHA1 Message Date
Nikhil Soni
4e0beac9b7 fix: correct check for flux interval in tracedetail apis 2026-04-16 10:39:04 +05:30
22 changed files with 146 additions and 405 deletions

View File

@@ -75,7 +75,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
},
signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(),
signoz.NewWebProviderFactories(config.Global),
signoz.NewWebProviderFactories(),
func(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
return signoz.NewSQLSchemaProviderFactories(sqlstore)
},

View File

@@ -96,7 +96,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
},
signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(),
signoz.NewWebProviderFactories(config.Global),
signoz.NewWebProviderFactories(),
func(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
existingFactories := signoz.NewSQLSchemaProviderFactories(sqlstore)
if err := existingFactories.Add(postgressqlschema.NewFactory(sqlstore)); err != nil {

View File

@@ -6,8 +6,6 @@
##################### Global #####################
global:
# the url under which the signoz apiserver is externally reachable.
# the path component (e.g. /signoz in https://example.com/signoz) is used
# as the base path for all HTTP routes (both API and web frontend).
external_url: <unset>
# the url where the SigNoz backend receives telemetry data (traces, metrics, logs) from instrumented applications.
ingestion_url: <unset>
@@ -52,8 +50,8 @@ pprof:
web:
# Whether to enable the web frontend
enabled: true
# The index file to use as the SPA entrypoint.
index: index.html
# The prefix to serve web on
prefix: /
# The directory containing the static build files.
directory: /etc/signoz/web

View File

@@ -262,20 +262,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
return nil, err
}
routePrefix := s.config.Global.ExternalPath()
if routePrefix != "" {
prefixed := http.StripPrefix(routePrefix, handler)
handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/api/v1/health", "/api/v2/healthz", "/api/v2/readyz", "/api/v2/livez":
r.ServeHTTP(w, req)
return
}
prefixed.ServeHTTP(w, req)
})
}
return &http.Server{
Handler: handler,
}, nil

View File

@@ -2,8 +2,6 @@ package global
import (
"net/url"
"path"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
@@ -39,34 +37,5 @@ func newConfig() factory.Config {
}
func (c Config) Validate() error {
if c.ExternalURL != nil {
if c.ExternalURL.Path != "" && c.ExternalURL.Path != "/" {
if !strings.HasPrefix(c.ExternalURL.Path, "/") {
return errors.NewInvalidInputf(ErrCodeInvalidGlobalConfig, "global::external_url path must start with '/', got %q", c.ExternalURL.Path)
}
}
}
return nil
}
func (c Config) ExternalPath() string {
if c.ExternalURL == nil || c.ExternalURL.Path == "" || c.ExternalURL.Path == "/" {
return ""
}
p := path.Clean("/" + c.ExternalURL.Path)
if p == "/" {
return ""
}
return p
}
func (c Config) ExternalPathTrailing() string {
if p := c.ExternalPath(); p != "" {
return p + "/"
}
return "/"
}

View File

@@ -1,139 +0,0 @@
package global
import (
"net/url"
"testing"
"github.com/stretchr/testify/assert"
)
func TestExternalPath(t *testing.T) {
testCases := []struct {
name string
config Config
expected string
}{
{
name: "NilURL",
config: Config{ExternalURL: nil},
expected: "",
},
{
name: "EmptyPath",
config: Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: ""}},
expected: "",
},
{
name: "RootPath",
config: Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/"}},
expected: "",
},
{
name: "SingleSegment",
config: Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/signoz"}},
expected: "/signoz",
},
{
name: "TrailingSlash",
config: Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/signoz/"}},
expected: "/signoz",
},
{
name: "MultiSegment",
config: Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/a/b/c"}},
expected: "/a/b/c",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.config.ExternalPath())
})
}
}
func TestExternalPathTrailing(t *testing.T) {
testCases := []struct {
name string
config Config
expected string
}{
{
name: "NilURL",
config: Config{ExternalURL: nil},
expected: "/",
},
{
name: "EmptyPath",
config: Config{ExternalURL: &url.URL{Path: ""}},
expected: "/",
},
{
name: "RootPath",
config: Config{ExternalURL: &url.URL{Path: "/"}},
expected: "/",
},
{
name: "SingleSegment",
config: Config{ExternalURL: &url.URL{Path: "/signoz"}},
expected: "/signoz/",
},
{
name: "MultiSegment",
config: Config{ExternalURL: &url.URL{Path: "/a/b/c"}},
expected: "/a/b/c/",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.config.ExternalPathTrailing())
})
}
}
func TestValidate(t *testing.T) {
testCases := []struct {
name string
config Config
fail bool
}{
{
name: "NilURL",
config: Config{ExternalURL: nil},
fail: false,
},
{
name: "EmptyPath",
config: Config{ExternalURL: &url.URL{Path: ""}},
fail: false,
},
{
name: "RootPath",
config: Config{ExternalURL: &url.URL{Path: "/"}},
fail: false,
},
{
name: "ValidPath",
config: Config{ExternalURL: &url.URL{Path: "/signoz"}},
fail: false,
},
{
name: "NoLeadingSlash",
config: Config{ExternalURL: &url.URL{Path: "signoz"}},
fail: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.config.Validate()
if tc.fail {
assert.Error(t, err)
return
}
assert.NoError(t, err)
})
}
}

View File

@@ -905,7 +905,7 @@ func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadataCache(ctx contex
return nil, err
}
if time.Since(time.UnixMilli(int64(cachedTraceData.EndTime))) < r.fluxIntervalForTraceDetail {
if time.Since(time.Unix(0, int64(cachedTraceData.EndTime))) < r.fluxIntervalForTraceDetail {
r.logger.Info("the trace end time falls under the flux interval, skipping getWaterfallSpansForTraceWithMetadata cache", "traceID", traceID)
return nil, errors.Errorf("the trace end time falls under the flux interval, skipping getWaterfallSpansForTraceWithMetadata cache, traceID: %s", traceID)
}
@@ -1138,7 +1138,7 @@ func (r *ClickHouseReader) GetFlamegraphSpansForTraceCache(ctx context.Context,
return nil, err
}
if time.Since(time.UnixMilli(int64(cachedTraceData.EndTime))) < r.fluxIntervalForTraceDetail {
if time.Since(time.Unix(0, int64(cachedTraceData.EndTime))) < r.fluxIntervalForTraceDetail {
r.logger.Info("the trace end time falls under the flux interval, skipping getFlamegraphSpansForTrace cache", "traceID", traceID)
return nil, errors.Errorf("the trace end time falls under the flux interval, skipping getFlamegraphSpansForTrace cache, traceID: %s", traceID)
}

View File

@@ -587,6 +587,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/query_filter/analyze", am.ViewAccess(aH.QueryParserAPI.AnalyzeQueryFilter)).Methods(http.MethodPost)
}
func Intersection(a, b []int) (c []int) {
m := make(map[int]bool)

View File

@@ -244,20 +244,6 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
return nil, err
}
routePrefix := s.config.Global.ExternalPath()
if routePrefix != "" {
prefixed := http.StripPrefix(routePrefix, handler)
handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/api/v1/health", "/api/v2/healthz", "/api/v2/readyz", "/api/v2/livez":
r.ServeHTTP(w, req)
return
}
prefixed.ServeHTTP(w, req)
})
}
return &http.Server{
Handler: handler,
}, nil

View File

@@ -88,9 +88,9 @@ func NewCacheProviderFactories() factory.NamedMap[factory.ProviderFactory[cache.
)
}
func NewWebProviderFactories(globalConfig global.Config) factory.NamedMap[factory.ProviderFactory[web.Web, web.Config]] {
func NewWebProviderFactories() factory.NamedMap[factory.ProviderFactory[web.Web, web.Config]] {
return factory.MustNewNamedMap(
routerweb.NewFactory(globalConfig),
routerweb.NewFactory(),
noopweb.NewFactory(),
)
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfmanagertest"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
@@ -35,7 +34,7 @@ func TestNewProviderFactories(t *testing.T) {
})
assert.NotPanics(t, func() {
NewWebProviderFactories(global.Config{})
NewWebProviderFactories()
})
assert.NotPanics(t, func() {

View File

@@ -8,11 +8,10 @@ import (
type Config struct {
// Whether the web package is enabled.
Enabled bool `mapstructure:"enabled"`
// The name of the index file to serve.
Index string `mapstructure:"index"`
// The directory from which to serve the web files.
// The prefix to serve the files from
Prefix string `mapstructure:"prefix"`
// The directory containing the static build files. The root of this directory should
// have an index.html file.
Directory string `mapstructure:"directory"`
}
@@ -23,7 +22,7 @@ func NewConfigFactory() factory.ConfigFactory {
func newConfig() factory.Config {
return &Config{
Enabled: true,
Index: "index.html",
Prefix: "/",
Directory: "/etc/signoz/web",
}
}

View File

@@ -12,6 +12,7 @@ import (
)
func TestNewWithEnvProvider(t *testing.T) {
t.Setenv("SIGNOZ_WEB_PREFIX", "/web")
t.Setenv("SIGNOZ_WEB_ENABLED", "false")
conf, err := config.New(
@@ -36,7 +37,7 @@ func TestNewWithEnvProvider(t *testing.T) {
expected := &Config{
Enabled: false,
Index: def.Index,
Prefix: "/web",
Directory: def.Directory,
}

View File

@@ -8,53 +8,56 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/web"
"github.com/gorilla/mux"
)
const (
indexFileName string = "index.html"
)
type provider struct {
config web.Config
indexContents []byte
config web.Config
}
func NewFactory(globalConfig global.Config) factory.ProviderFactory[web.Web, web.Config] {
return factory.NewProviderFactory(factory.MustNewName("router"), func(ctx context.Context, settings factory.ProviderSettings, config web.Config) (web.Web, error) {
return New(ctx, settings, config, globalConfig)
})
func NewFactory() factory.ProviderFactory[web.Web, web.Config] {
return factory.NewProviderFactory(factory.MustNewName("router"), New)
}
func New(ctx context.Context, settings factory.ProviderSettings, config web.Config, globalConfig global.Config) (web.Web, error) {
func New(ctx context.Context, settings factory.ProviderSettings, config web.Config) (web.Web, error) {
fi, err := os.Stat(config.Directory)
if err != nil {
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "cannot access web directory")
}
if !fi.IsDir() {
ok := fi.IsDir()
if !ok {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "web directory is not a directory")
}
indexPath := filepath.Join(config.Directory, config.Index)
raw, err := os.ReadFile(indexPath)
fi, err = os.Stat(filepath.Join(config.Directory, indexFileName))
if err != nil {
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "cannot read %q in web directory", config.Index)
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "cannot access %q in web directory", indexFileName)
}
logger := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/web/routerweb").Logger()
indexContents := web.NewIndex(ctx, logger, config.Index, raw, web.TemplateData{BaseHref: globalConfig.ExternalPathTrailing()})
if os.IsNotExist(err) || fi.IsDir() {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "%q does not exist", indexFileName)
}
return &provider{
config: config,
indexContents: indexContents,
config: config,
}, nil
}
func (provider *provider) AddToRouter(router *mux.Router) error {
cache := middleware.NewCache(0)
err := router.PathPrefix("/").
err := router.PathPrefix(provider.config.Prefix).
Handler(
cache.Wrap(http.HandlerFunc(provider.ServeHTTP)),
http.StripPrefix(
provider.config.Prefix,
cache.Wrap(http.HandlerFunc(provider.ServeHTTP)),
),
).GetError()
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "unable to add web to router")
@@ -72,7 +75,7 @@ func (provider *provider) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if err != nil {
// if the file doesn't exist, serve index.html
if os.IsNotExist(err) {
provider.serveIndex(rw)
http.ServeFile(rw, req, filepath.Join(provider.config.Directory, indexFileName))
return
}
@@ -84,15 +87,10 @@ func (provider *provider) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if fi.IsDir() {
// path is a directory, serve index.html
provider.serveIndex(rw)
http.ServeFile(rw, req, filepath.Join(provider.config.Directory, indexFileName))
return
}
// otherwise, use http.FileServer to serve the static file
http.FileServer(http.Dir(provider.config.Directory)).ServeHTTP(rw, req)
}
func (provider *provider) serveIndex(rw http.ResponseWriter) {
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = rw.Write(provider.indexContents)
}

View File

@@ -5,113 +5,45 @@ import (
"io"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"github.com/SigNoz/signoz/pkg/factory/factorytest"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/web"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func startServer(t *testing.T, config web.Config, globalConfig global.Config) string {
t.Helper()
func TestServeHttpWithoutPrefix(t *testing.T) {
t.Parallel()
fi, err := os.Open(filepath.Join("testdata", indexFileName))
require.NoError(t, err)
web, err := New(context.Background(), factorytest.NewSettings(), config, globalConfig)
expected, err := io.ReadAll(fi)
require.NoError(t, err)
web, err := New(context.Background(), factorytest.NewSettings(), web.Config{Prefix: "/", Directory: filepath.Join("testdata")})
require.NoError(t, err)
router := mux.NewRouter()
require.NoError(t, web.AddToRouter(router))
err = web.AddToRouter(router)
require.NoError(t, err)
listener, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
server := &http.Server{Handler: router}
go func() { _ = server.Serve(listener) }()
t.Cleanup(func() { _ = server.Close() })
return "http://" + listener.Addr().String()
}
func httpGet(t *testing.T, url string) string {
t.Helper()
res, err := http.DefaultClient.Get(url)
require.NoError(t, err)
defer func() { _ = res.Body.Close() }()
body, err := io.ReadAll(res.Body)
require.NoError(t, err)
return string(body)
}
func TestServeTemplatedIndex(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
path string
globalConfig global.Config
expected string
}{
{
name: "RootBaseHrefAtRoot",
path: "/",
globalConfig: global.Config{},
expected: `<html><head><base href="/" /></head><body>Welcome to test data!!!</body></html>`,
},
{
name: "RootBaseHrefAtNonExistentPath",
path: "/does-not-exist",
globalConfig: global.Config{},
expected: `<html><head><base href="/" /></head><body>Welcome to test data!!!</body></html>`,
},
{
name: "RootBaseHrefAtDirectory",
path: "/assets",
globalConfig: global.Config{},
expected: `<html><head><base href="/" /></head><body>Welcome to test data!!!</body></html>`,
},
{
name: "SubPathBaseHrefAtRoot",
path: "/",
globalConfig: global.Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/signoz"}},
expected: `<html><head><base href="/signoz/" /></head><body>Welcome to test data!!!</body></html>`,
},
{
name: "SubPathBaseHrefAtNonExistentPath",
path: "/does-not-exist",
globalConfig: global.Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/signoz"}},
expected: `<html><head><base href="/signoz/" /></head><body>Welcome to test data!!!</body></html>`,
},
{
name: "SubPathBaseHrefAtDirectory",
path: "/assets",
globalConfig: global.Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/signoz"}},
expected: `<html><head><base href="/signoz/" /></head><body>Welcome to test data!!!</body></html>`,
},
server := &http.Server{
Handler: router,
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
base := startServer(t, web.Config{Index: "valid_template.html", Directory: "testdata"}, testCase.globalConfig)
assert.Equal(t, testCase.expected, strings.TrimSuffix(httpGet(t, base+testCase.path), "\n"))
})
}
}
func TestServeNoTemplateIndex(t *testing.T) {
t.Parallel()
expected, err := os.ReadFile(filepath.Join("testdata", "no_template.html"))
require.NoError(t, err)
go func() {
_ = server.Serve(listener)
}()
defer func() {
_ = server.Close()
}()
testCases := []struct {
name string
@@ -122,7 +54,11 @@ func TestServeNoTemplateIndex(t *testing.T) {
path: "/",
},
{
name: "NonExistentPath",
name: "Index",
path: "/" + indexFileName,
},
{
name: "DoesNotExist",
path: "/does-not-exist",
},
{
@@ -131,55 +67,104 @@ func TestServeNoTemplateIndex(t *testing.T) {
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
base := startServer(t, web.Config{Index: "no_template.html", Directory: "testdata"}, global.Config{})
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
res, err := http.DefaultClient.Get("http://" + listener.Addr().String() + tc.path)
require.NoError(t, err)
assert.Equal(t, string(expected), httpGet(t, base+testCase.path))
defer func() {
_ = res.Body.Close()
}()
actual, err := io.ReadAll(res.Body)
require.NoError(t, err)
assert.Equal(t, expected, actual)
})
}
}
func TestServeInvalidTemplateIndex(t *testing.T) {
func TestServeHttpWithPrefix(t *testing.T) {
t.Parallel()
expected, err := os.ReadFile(filepath.Join("testdata", "invalid_template.html"))
fi, err := os.Open(filepath.Join("testdata", indexFileName))
require.NoError(t, err)
expected, err := io.ReadAll(fi)
require.NoError(t, err)
web, err := New(context.Background(), factorytest.NewSettings(), web.Config{Prefix: "/web", Directory: filepath.Join("testdata")})
require.NoError(t, err)
router := mux.NewRouter()
err = web.AddToRouter(router)
require.NoError(t, err)
listener, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
server := &http.Server{
Handler: router,
}
go func() {
_ = server.Serve(listener)
}()
defer func() {
_ = server.Close()
}()
testCases := []struct {
name string
path string
name string
path string
found bool
}{
{
name: "Root",
path: "/",
name: "Root",
path: "/web",
found: true,
},
{
name: "NonExistentPath",
path: "/does-not-exist",
name: "Index",
path: "/web/" + indexFileName,
found: true,
},
{
name: "Directory",
path: "/assets",
name: "FileDoesNotExist",
path: "/web/does-not-exist",
found: true,
},
{
name: "Directory",
path: "/web/assets",
found: true,
},
{
name: "DoesNotExist",
path: "/does-not-exist",
found: false,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
base := startServer(t, web.Config{Index: "invalid_template.html", Directory: "testdata"}, global.Config{ExternalURL: &url.URL{Path: "/signoz"}})
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
res, err := http.DefaultClient.Get("http://" + listener.Addr().String() + tc.path)
require.NoError(t, err)
defer func() {
_ = res.Body.Close()
}()
if tc.found {
actual, err := io.ReadAll(res.Body)
require.NoError(t, err)
assert.Equal(t, expected, actual)
} else {
assert.Equal(t, http.StatusNotFound, res.StatusCode)
}
assert.Equal(t, string(expected), httpGet(t, base+testCase.path))
})
}
}
func TestServeStaticFilesUnchanged(t *testing.T) {
t.Parallel()
expected, err := os.ReadFile(filepath.Join("testdata", "assets", "style.css"))
require.NoError(t, err)
base := startServer(t, web.Config{Index: "valid_template.html", Directory: "testdata"}, global.Config{ExternalURL: &url.URL{Path: "/signoz"}})
assert.Equal(t, string(expected), httpGet(t, base+"/assets/style.css"))
}

View File

@@ -0,0 +1,3 @@
#root {
background-color: red;
}

View File

@@ -1 +0,0 @@
body { color: red; }

1
pkg/web/routerweb/testdata/index.html vendored Normal file
View File

@@ -0,0 +1 @@
<h1>Welcome to test data!!!</h1>

View File

@@ -1 +0,0 @@
<html><head><base href="[[." /></head><body>Bad template</body></html>

View File

@@ -1 +0,0 @@
<html><head></head><body>No template here</body></html>

View File

@@ -1 +0,0 @@
<html><head><base href="[[.BaseHref]]" /></head><body>Welcome to test data!!!</body></html>

View File

@@ -1,42 +0,0 @@
package web
import (
"bytes"
"context"
"log/slog"
"text/template"
"github.com/SigNoz/signoz/pkg/errors"
)
// Field names map to the HTML attributes they populate in the template:
// - BaseHref → <base href="[[.BaseHref]]" />
type TemplateData struct {
BaseHref string
}
// If the template cannot be parsed or executed, the raw bytes are
// returned unchanged and the error is logged.
func NewIndex(ctx context.Context, logger *slog.Logger, name string, raw []byte, data TemplateData) []byte {
result, err := NewIndexE(name, raw, data)
if err != nil {
logger.ErrorContext(ctx, "cannot render index template, serving raw file", slog.String("name", name), errors.Attr(err))
return raw
}
return result
}
func NewIndexE(name string, raw []byte, data TemplateData) ([]byte, error) {
tmpl, err := template.New(name).Delims("[[", "]]").Parse(string(raw))
if err != nil {
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "cannot parse %q as template", name)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "cannot execute template for %q", name)
}
return buf.Bytes(), nil
}