Skip to content

Commit

Permalink
Add ability to use JS file for UI configuration (jaeger-ui/#123) (jae…
Browse files Browse the repository at this point in the history
…gertracing#2707)

* Add ability to use JS file for UI configuration (#123 from jaeger-ui)

Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>

* Update UI config file injection

Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>

* Update update test, refactor loadUIConfig function

Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>

* Update errors text, minor refactor

Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>

* Remove debug code

Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>

* Update jaeger-ui to latest master with index.html changes

Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>
  • Loading branch information
th3M1ke authored and bhiravabhatla committed Jan 25, 2021
1 parent 5b80343 commit dc1b855
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 41 deletions.
8 changes: 6 additions & 2 deletions cmd/query/app/fixture/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
<meta charset="UTF-8">
<base href="/" data-inject-target="BASE_URL"/>
<title>Test Page</title>
<!-- JAEGER_CONFIG=DEFAULT_CONFIG; -->
<!-- JAEGER_VERSION=DEFAULT_VERSION; -->
<!--
// 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_VERSION=DEFAULT_VERSION;
-->
</html>
10 changes: 10 additions & 0 deletions cmd/query/app/fixture/ui-config-malformed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
() => {
return {
menu: [
{
label: "GitHub",
url: "https://github.com/jaegertracing/jaeger"
}
]
}
}
10 changes: 10 additions & 0 deletions cmd/query/app/fixture/ui-config-menu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
function UIConfig(){
return {
menu: [
{
label: "GitHub",
url: "https://github.com/jaegertracing/jaeger"
}
]
}
}
5 changes: 5 additions & 0 deletions cmd/query/app/fixture/ui-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function UIConfig(){
return {
x: "y"
}
}
59 changes: 37 additions & 22 deletions cmd/query/app/static_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package app

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
Expand All @@ -40,6 +41,7 @@ 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
)
Expand Down Expand Up @@ -75,6 +77,11 @@ type StaticAssetsHandlerOptions struct {
NewWatcher func() (fswatcher.Watcher, error)
}

type loadedConfig struct {
regexp *regexp.Regexp
config []byte
}

// NewStaticAssetsHandler returns a StaticAssetsHandler
func NewStaticAssetsHandler(staticAssetsRoot string, options StaticAssetsHandlerOptions) (*StaticAssetsHandler, error) {
assetsFS := ui.StaticFiles
Expand Down Expand Up @@ -113,17 +120,11 @@ func loadAndEnrichIndexHTML(open func(string) (http.File, error), options Static
return nil, fmt.Errorf("cannot load index.html: %w", err)
}
// replace UI config
configString := "JAEGER_CONFIG = DEFAULT_CONFIG"
if config, err := loadUIConfig(options.UIConfigPath); err != nil {
if configObject, err := loadUIConfig(options.UIConfigPath); err != nil {
return nil, err
} else if config != nil {
// TODO if we want to support other config formats like YAML, we need to normalize `config` to be
// suitable for json.Marshal(). For example, YAML parser may return a map that has keys of type
// interface{}, and json.Marshal() is unable to serialize it.
bytes, _ := json.Marshal(config)
configString = fmt.Sprintf("JAEGER_CONFIG = %v", string(bytes))
} else if configObject != nil {
indexBytes = configObject.regexp.ReplaceAll(indexBytes, configObject.config)
}
indexBytes = configPattern.ReplaceAll(indexBytes, []byte(configString+";"))
// replace Jaeger version
versionJSON, _ := json.Marshal(version.Get())
versionString := fmt.Sprintf("JAEGER_VERSION = %s;", string(versionJSON))
Expand Down Expand Up @@ -218,30 +219,44 @@ func loadIndexHTML(open func(string) (http.File, error)) ([]byte, error) {
return indexBytes, nil
}

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

var c map[string]interface{}
var unmarshal func([]byte, interface{}) error

ext := filepath.Ext(uiConfig)
switch strings.ToLower(ext) {
case ".json":
unmarshal = json.Unmarshal
default:
return nil, fmt.Errorf("unrecognized UI config file format %v", uiConfig)
}
var c map[string]interface{}

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

return &loadedConfig{
regexp: configPattern,
config: append([]byte("JAEGER_CONFIG = "), append(r, byte(';'))...),
}, nil
case ".js":
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 &loadedConfig{
regexp: configJsPattern,
config: r,
}, nil
default:
return nil, fmt.Errorf("unrecognized UI config file format, expecting .js or .json file: %v", uiConfig)
}
return c, nil
}

// RegisterRoutes registers routes for this handler on the given router
Expand Down
90 changes: 74 additions & 16 deletions cmd/query/app/static_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package app

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
Expand Down Expand Up @@ -63,10 +64,31 @@ func TestRegisterStaticHandler(t *testing.T) {
subroute bool // should we create a subroute?
baseURL string // expected URL prefix
expectedBaseHTML string // substring to match in the home page
UIConfigPath string // path to UI config
expectedUIConfig string // expected UI config
}{
{basePath: "", baseURL: "/", expectedBaseHTML: `<base href="/"`},
{basePath: "/", baseURL: "/", expectedBaseHTML: `<base href="/"`},
{basePath: "/jaeger", baseURL: "/jaeger/", expectedBaseHTML: `<base href="/jaeger/"`, subroute: true},
{
basePath: "",
baseURL: "/",
expectedBaseHTML: `<base href="/"`,
UIConfigPath: "",
expectedUIConfig: "JAEGER_CONFIG=DEFAULT_CONFIG;",
},
{
basePath: "/",
baseURL: "/",
expectedBaseHTML: `<base href="/"`,
UIConfigPath: "fixture/ui-config.json",
expectedUIConfig: `JAEGER_CONFIG = {"x":"y"};`,
},
{
basePath: "/jaeger",
baseURL: "/jaeger/",
expectedBaseHTML: `<base href="/jaeger/"`,
subroute: true,
UIConfigPath: "fixture/ui-config.js",
expectedUIConfig: "function UIConfig(){",
},
}
httpClient = &http.Client{
Timeout: 2 * time.Second,
Expand All @@ -81,7 +103,7 @@ func TestRegisterStaticHandler(t *testing.T) {
RegisterStaticHandler(r, logger, &QueryOptions{
StaticAssets: "fixture",
BasePath: testCase.basePath,
UIConfig: "fixture/ui-config.json",
UIConfig: testCase.UIConfigPath,
})

server := httptest.NewServer(r)
Expand All @@ -103,7 +125,7 @@ func TestRegisterStaticHandler(t *testing.T) {
assert.Contains(t, respString, "Test Favicon") // this text is present in fixtures/favicon.ico

html := httpGet("") // get home page
assert.Contains(t, html, `JAEGER_CONFIG = {"x":"y"};`, "actual: %v", html)
assert.Contains(t, html, testCase.expectedUIConfig, "actual: %v", html)
assert.Contains(t, html, `JAEGER_VERSION = {"gitCommit":"","gitVersion":"","buildDate":""};`, "actual: %v", html)
assert.Contains(t, html, testCase.expectedBaseHTML, "actual: %v", html)

Expand Down Expand Up @@ -260,7 +282,7 @@ func TestHotReloadUIConfigTempFile(t *testing.T) {
func TestLoadUIConfig(t *testing.T) {
type testCase struct {
configFile string
expected map[string]interface{}
expected *loadedConfig
expectedError string
}

Expand All @@ -277,33 +299,69 @@ func TestLoadUIConfig(t *testing.T) {
}

run("no config", testCase{})
run("invalid config", testCase{
run("invalid json config", testCase{
configFile: "invalid",
expectedError: "cannot read UI config file invalid: open invalid: no such file or directory",
})
run("unsupported type", testCase{
configFile: "fixture/ui-config.toml",
expectedError: "unrecognized UI config file format fixture/ui-config.toml",
expectedError: "unrecognized UI config file format, expecting .js or .json file: fixture/ui-config.toml",
})
run("malformed", testCase{
configFile: "fixture/ui-config-malformed.json",
expectedError: "cannot parse UI config file fixture/ui-config-malformed.json: invalid character '=' after object key",
})
run("json", testCase{
configFile: "fixture/ui-config.json",
expected: map[string]interface{}{"x": "y"},
expected: &loadedConfig{
config: []byte(`JAEGER_CONFIG = {"x":"y"};`),
regexp: configPattern,
},
})
c, _ := json.Marshal(map[string]interface{}{
"menu": []interface{}{
map[string]interface{}{
"label": "GitHub",
"url": "https://github.com/jaegertracing/jaeger",
},
},
})
run("json-menu", testCase{
configFile: "fixture/ui-config-menu.json",
expected: map[string]interface{}{
"menu": []interface{}{
map[string]interface{}{
"label": "GitHub",
"url": "https://github.com/jaegertracing/jaeger",
},
},
expected: &loadedConfig{
config: append([]byte("JAEGER_CONFIG = "), append(c, byte(';'))...),
regexp: configPattern,
},
})
run("malformed js config", testCase{
configFile: "fixture/ui-config-malformed.js",
expectedError: "UI config file must define function UIConfig(): fixture/ui-config-malformed.js",
})
run("js", testCase{
configFile: "fixture/ui-config.js",
expected: &loadedConfig{
regexp: configJsPattern,
config: []byte(`function UIConfig(){
return {
x: "y"
}
}`)},
})
run("js-menu", testCase{
configFile: "fixture/ui-config-menu.js",
expected: &loadedConfig{
regexp: configJsPattern,
config: []byte(`function UIConfig(){
return {
menu: [
{
label: "GitHub",
url: "https://github.com/jaegertracing/jaeger"
}
]
}
}`)},
})
}

type fakeFile struct {
Expand Down

0 comments on commit dc1b855

Please sign in to comment.