Skip to content

Commit

Permalink
Make ArchiveTrace button auto-configurable
Browse files Browse the repository at this point in the history
- Resolves #4874

The button to archive a trace is now configured based on the state of
the QueryService in addition to the UI configuration. It is now
possible to request features from the QueryService to inject them
into the UI.

All corresponding tests have been updated.

- [X] I have read
https://github.com/jaegertracing/jaeger/blob/master/CONTRIBUTING_GUIDELINES.md
- [X] I have signed all commits
- [X] I have added unit tests for the new functionality
- [X] I have run lint and test steps successfully

---------

Signed-off-by: Barthelemy Antonin <antobarth@gmail.com>
  • Loading branch information
thecoons committed Nov 3, 2023
1 parent f99eae5 commit 6e441f0
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 106 deletions.
10 changes: 6 additions & 4 deletions cmd/query/app/fixture/index.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<base href="/" data-inject-target="BASE_URL"/>
<title>Test Page</title>
<!--
<meta charset="UTF-8">
<base href="/" data-inject-target="BASE_URL" />
<title>Test Page</title>
<!--
// JAEGER_CONFIG_JS
// the line above may be replaced by user-provided JS file that should define a UIConfig function.
JAEGER_CONFIG=DEFAULT_CONFIG;
JAEGER_STORAGE_CAPABILITIES=DEFAULT_STORAGE_CAPABILITIES;
JAEGER_VERSION=DEFAULT_VERSION;
-->

</html>
19 changes: 19 additions & 0 deletions cmd/query/app/querysvc/query_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ type QueryServiceOptions struct {
Adjuster adjuster.Adjuster
}

// StorageCapabilities is a feature flag for query service
type StorageCapabilities struct {
ArchiveStorage bool `json:"archiveStorage"`
// SupportRegex bool
// SupportTagFilter bool
}

// QueryService contains span utils required by the query-service.
type QueryService struct {
spanReader spanstore.Reader
Expand Down Expand Up @@ -122,6 +129,13 @@ func (qs QueryService) GetDependencies(ctx context.Context, endTs time.Time, loo
return qs.dependencyReader.GetDependencies(ctx, endTs, lookback)
}

// GetCapabilities returns the features supported by the query service.
func (qs QueryService) GetCapabilities() StorageCapabilities {
return StorageCapabilities{
ArchiveStorage: qs.options.hasArchiveStorage(),
}
}

// InitArchiveStorage tries to initialize archive storage reader/writer if storage factory supports them.
func (opts *QueryServiceOptions) InitArchiveStorage(storageFactory storage.Factory, logger *zap.Logger) bool {
archiveFactory, ok := storageFactory.(storage.ArchiveFactory)
Expand Down Expand Up @@ -151,3 +165,8 @@ func (opts *QueryServiceOptions) InitArchiveStorage(storageFactory storage.Facto
opts.ArchiveSpanWriter = writer
return true
}

// hasArchiveStorage returns true if archive storage reader/writer are initialized.
func (opts *QueryServiceOptions) hasArchiveStorage() bool {
return opts.ArchiveSpanReader != nil && opts.ArchiveSpanWriter != nil
}
18 changes: 18 additions & 0 deletions cmd/query/app/querysvc/query_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,24 @@ func TestGetDependencies(t *testing.T) {
assert.Equal(t, expectedDependencies, actualDependencies)
}

// Test QueryService.GetFeatures()
func TestGetFeatures(t *testing.T) {
tqs := initializeTestService()
expectedStorageCapabilities := StorageCapabilities{
ArchiveStorage: false,
}
assert.Equal(t, expectedStorageCapabilities, tqs.queryService.GetCapabilities())
}

func TestGetFeaturesWithSupportsArchive(t *testing.T) {
tqs := initializeTestService(withArchiveSpanReader(), withArchiveSpanWriter())

expectedStorageCapabilities := StorageCapabilities{
ArchiveStorage: true,
}
assert.Equal(t, expectedStorageCapabilities, tqs.queryService.GetCapabilities())
}

type fakeStorageFactory1 struct{}

type fakeStorageFactory2 struct {
Expand Down
2 changes: 1 addition & 1 deletion cmd/query/app/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ func createHTTPServer(querySvc *querysvc.QueryService, metricsQuerySvc querysvc.
}

apiHandler.RegisterRoutes(r)
RegisterStaticHandler(r, logger, queryOpts)
RegisterStaticHandler(r, logger, queryOpts, querySvc.GetCapabilities())
var handler http.Handler = r
handler = additionalHeadersHandler(handler, queryOpts.AdditionalHeaders)
if queryOpts.BearerTokenPropagation {
Expand Down
158 changes: 83 additions & 75 deletions cmd/query/app/static_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,29 @@ import (
"github.com/gorilla/mux"
"go.uber.org/zap"

"github.com/jaegertracing/jaeger/cmd/query/app/querysvc"
"github.com/jaegertracing/jaeger/cmd/query/app/ui"
"github.com/jaegertracing/jaeger/pkg/fswatcher"
"github.com/jaegertracing/jaeger/pkg/version"
)

var (
// The following patterns are searched and replaced in the index.html as a way of customizing the UI.
configPattern = regexp.MustCompile("JAEGER_CONFIG *= *DEFAULT_CONFIG;")
configJsPattern = regexp.MustCompile(`(?im)^\s*\/\/\s*JAEGER_CONFIG_JS.*\n.*`)
versionPattern = regexp.MustCompile("JAEGER_VERSION *= *DEFAULT_VERSION;")
basePathPattern = regexp.MustCompile(`<base href="/"`) // Note: tag is not closed
configPattern = regexp.MustCompile("JAEGER_CONFIG *= *DEFAULT_CONFIG;")
configJsPattern = regexp.MustCompile(`(?im)^\s*\/\/\s*JAEGER_CONFIG_JS.*\n.*`)
versionPattern = regexp.MustCompile("JAEGER_VERSION *= *DEFAULT_VERSION;")
compabilityPattern = regexp.MustCompile("JAEGER_STORAGE_CAPABILITIES *= *DEFAULT_STORAGE_CAPABILITIES;")
basePathPattern = regexp.MustCompile(`<base href="/"`) // Note: tag is not closed
)

// RegisterStaticHandler adds handler for static assets to the router.
func RegisterStaticHandler(r *mux.Router, logger *zap.Logger, qOpts *QueryOptions) {
func RegisterStaticHandler(r *mux.Router, logger *zap.Logger, qOpts *QueryOptions, qFeatures querysvc.StorageCapabilities) {
staticHandler, err := NewStaticAssetsHandler(qOpts.StaticAssets.Path, StaticAssetsHandlerOptions{
BasePath: qOpts.BasePath,
UIConfigPath: qOpts.UIConfig,
Logger: logger,
LogAccess: qOpts.StaticAssets.LogAccess,
BasePath: qOpts.BasePath,
UIConfigPath: qOpts.UIConfig,
StorageCapabilities: qFeatures,
Logger: logger,
LogAccess: qOpts.StaticAssets.LogAccess,
})
if err != nil {
logger.Panic("Could not create static assets handler", zap.Error(err))
Expand All @@ -68,10 +71,11 @@ type StaticAssetsHandler struct {

// StaticAssetsHandlerOptions defines options for NewStaticAssetsHandler
type StaticAssetsHandlerOptions struct {
BasePath string
UIConfigPath string
LogAccess bool
Logger *zap.Logger
BasePath string
UIConfigPath string
LogAccess bool
StorageCapabilities querysvc.StorageCapabilities
Logger *zap.Logger
}

type loadedConfig struct {
Expand All @@ -90,7 +94,7 @@ func NewStaticAssetsHandler(staticAssetsRoot string, options StaticAssetsHandler
options.Logger = zap.NewNop()
}

indexHTML, err := loadAndEnrichIndexHTML(assetsFS.Open, options)
indexHTML, err := options.loadAndEnrichIndexHTML(assetsFS.Open)
if err != nil {
return nil, err
}
Expand All @@ -112,75 +116,92 @@ func NewStaticAssetsHandler(staticAssetsRoot string, options StaticAssetsHandler
return h, nil
}

func loadAndEnrichIndexHTML(open func(string) (http.File, error), options StaticAssetsHandlerOptions) ([]byte, error) {
func (sH *StaticAssetsHandler) reloadUIConfig() {
sH.options.Logger.Info("reloading UI config", zap.String("filename", sH.options.UIConfigPath))
content, err := sH.options.loadAndEnrichIndexHTML(sH.assetsFS.Open)
if err != nil {
sH.options.Logger.Error("error while reloading the UI config", zap.Error(err))
}
sH.indexHTML.Store(content)
sH.options.Logger.Info("reloaded UI config", zap.String("filename", sH.options.UIConfigPath))
}

func (sH *StaticAssetsHandler) loggingHandler(handler http.Handler) http.Handler {
if !sH.options.LogAccess {
return handler
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sH.options.Logger.Info("serving static asset", zap.Stringer("url", r.URL))
handler.ServeHTTP(w, r)
})
}

// RegisterRoutes registers routes for this handler on the given router
func (sH *StaticAssetsHandler) RegisterRoutes(router *mux.Router) {
fileServer := http.FileServer(sH.assetsFS)
if sH.options.BasePath != "/" {
fileServer = http.StripPrefix(sH.options.BasePath+"/", fileServer)
}
router.PathPrefix("/static/").Handler(sH.loggingHandler(fileServer))
// index.html is served by notFound handler
router.NotFoundHandler = sH.loggingHandler(http.HandlerFunc(sH.notFound))
}

func (sH *StaticAssetsHandler) notFound(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(sH.indexHTML.Load().([]byte))
}

func (sHOpts *StaticAssetsHandlerOptions) loadAndEnrichIndexHTML(open func(string) (http.File, error)) ([]byte, error) {
indexBytes, err := loadIndexHTML(open)
if err != nil {
return nil, fmt.Errorf("cannot load index.html: %w", err)
}
// replace UI config
if configObject, err := loadUIConfig(options.UIConfigPath); err != nil {
if configObject, err := sHOpts.loadUIConfig(); err != nil {
return nil, err
} else if configObject != nil {
indexBytes = configObject.regexp.ReplaceAll(indexBytes, configObject.config)
}
// replace storage capabilities
capabilitiesJSON, _ := json.Marshal(sHOpts.StorageCapabilities)
capabilitiesString := fmt.Sprintf("JAEGER_STORAGE_CAPABILITIES = %s;", string(capabilitiesJSON))
indexBytes = compabilityPattern.ReplaceAll(indexBytes, []byte(capabilitiesString))
// replace Jaeger version
versionJSON, _ := json.Marshal(version.Get())
versionString := fmt.Sprintf("JAEGER_VERSION = %s;", string(versionJSON))
indexBytes = versionPattern.ReplaceAll(indexBytes, []byte(versionString))
// replace base path
if options.BasePath == "" {
options.BasePath = "/"
if sHOpts.BasePath == "" {
sHOpts.BasePath = "/"
}
if options.BasePath != "/" {
if !strings.HasPrefix(options.BasePath, "/") || strings.HasSuffix(options.BasePath, "/") {
return nil, fmt.Errorf("invalid base path '%s'. Must start but not end with a slash '/', e.g. '/jaeger/ui'", options.BasePath)
if sHOpts.BasePath != "/" {
if !strings.HasPrefix(sHOpts.BasePath, "/") || strings.HasSuffix(sHOpts.BasePath, "/") {
return nil, fmt.Errorf("invalid base path '%s'. Must start but not end with a slash '/', e.g. '/jaeger/ui'", sHOpts.BasePath)
}
indexBytes = basePathPattern.ReplaceAll(indexBytes, []byte(fmt.Sprintf(`<base href="%s/"`, options.BasePath)))
}

return indexBytes, nil
}

func (sH *StaticAssetsHandler) reloadUIConfig() {
sH.options.Logger.Info("reloading UI config", zap.String("filename", sH.options.UIConfigPath))
content, err := loadAndEnrichIndexHTML(sH.assetsFS.Open, sH.options)
if err != nil {
sH.options.Logger.Error("error while reloading the UI config", zap.Error(err))
indexBytes = basePathPattern.ReplaceAll(indexBytes, []byte(fmt.Sprintf(`<base href="%s/"`, sHOpts.BasePath)))
}
sH.indexHTML.Store(content)
sH.options.Logger.Info("reloaded UI config", zap.String("filename", sH.options.UIConfigPath))
}

func loadIndexHTML(open func(string) (http.File, error)) ([]byte, error) {
indexFile, err := open("/index.html")
if err != nil {
return nil, fmt.Errorf("cannot open index.html: %w", err)
}
defer indexFile.Close()
indexBytes, err := io.ReadAll(indexFile)
if err != nil {
return nil, fmt.Errorf("cannot read from index.html: %w", err)
}
return indexBytes, nil
}

func loadUIConfig(uiConfig string) (*loadedConfig, error) {
if uiConfig == "" {
func (sHOpts *StaticAssetsHandlerOptions) loadUIConfig() (*loadedConfig, error) {
if sHOpts.UIConfigPath == "" {
return nil, nil
}
bytesConfig, err := os.ReadFile(filepath.Clean(uiConfig))
bytesConfig, err := os.ReadFile(filepath.Clean(sHOpts.UIConfigPath))
if err != nil {
return nil, fmt.Errorf("cannot read UI config file %v: %w", uiConfig, err)
return nil, fmt.Errorf("cannot read UI config file %v: %w", sHOpts.UIConfigPath, err)
}
var r []byte

ext := filepath.Ext(uiConfig)
ext := filepath.Ext(sHOpts.UIConfigPath)
switch strings.ToLower(ext) {
case ".json":
var c map[string]interface{}

if err := json.Unmarshal(bytesConfig, &c); err != nil {
return nil, fmt.Errorf("cannot parse UI config file %v: %w", uiConfig, err)
return nil, fmt.Errorf("cannot parse UI config file %v: %w", sHOpts.UIConfigPath, err)
}
r, _ = json.Marshal(c)

Expand All @@ -192,40 +213,27 @@ func loadUIConfig(uiConfig string) (*loadedConfig, error) {
r = bytes.TrimSpace(bytesConfig)
re := regexp.MustCompile(`function\s+UIConfig(\s)?\(\s?\)(\s)?{`)
if !re.Match(r) {
return nil, fmt.Errorf("UI config file must define function UIConfig(): %v", uiConfig)
return nil, fmt.Errorf("UI config file must define function UIConfig(): %v", sHOpts.UIConfigPath)
}

return &loadedConfig{
regexp: configJsPattern,
config: r,
}, nil
default:
return nil, fmt.Errorf("unrecognized UI config file format, expecting .js or .json file: %v", uiConfig)
return nil, fmt.Errorf("unrecognized UI config file format, expecting .js or .json file: %v", sHOpts.UIConfigPath)
}
}

func (sH *StaticAssetsHandler) loggingHandler(handler http.Handler) http.Handler {
if !sH.options.LogAccess {
return handler
func loadIndexHTML(open func(string) (http.File, error)) ([]byte, error) {
indexFile, err := open("/index.html")
if err != nil {
return nil, fmt.Errorf("cannot open index.html: %w", err)
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sH.options.Logger.Info("serving static asset", zap.Stringer("url", r.URL))
handler.ServeHTTP(w, r)
})
}

// RegisterRoutes registers routes for this handler on the given router
func (sH *StaticAssetsHandler) RegisterRoutes(router *mux.Router) {
fileServer := http.FileServer(sH.assetsFS)
if sH.options.BasePath != "/" {
fileServer = http.StripPrefix(sH.options.BasePath+"/", fileServer)
defer indexFile.Close()
indexBytes, err := io.ReadAll(indexFile)
if err != nil {
return nil, fmt.Errorf("cannot read from index.html: %w", err)
}
router.PathPrefix("/static/").Handler(sH.loggingHandler(fileServer))
// index.html is served by notFound handler
router.NotFoundHandler = sH.loggingHandler(http.HandlerFunc(sH.notFound))
}

func (sH *StaticAssetsHandler) notFound(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(sH.indexHTML.Load().([]byte))
return indexBytes, nil
}
Loading

0 comments on commit 6e441f0

Please sign in to comment.