diff --git a/.travis.yml b/.travis.yml
index d4a1bacf0bc..d0951b9bdb8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -53,3 +53,6 @@ script:
after_success:
- if [ "$COVERAGE" == true ]; then travis_retry goveralls -coverprofile=cover.out -service=travis-ci || true ; else echo 'skipping coverage'; fi
+
+after_failure:
+ - if [ "$CROSSDOCK" == true ]; then make crossdock-logs ; else echo 'skipping crossdock'; fi
diff --git a/cmd/query/app/builder/builder_flags.go b/cmd/query/app/builder/builder_flags.go
index 88f4de1fb8b..89889c250ac 100644
--- a/cmd/query/app/builder/builder_flags.go
+++ b/cmd/query/app/builder/builder_flags.go
@@ -24,19 +24,22 @@ const (
queryPort = "query.port"
queryPrefix = "query.prefix"
queryStaticFiles = "query.static-files"
+ queryUIConfig = "query.ui-config"
queryHealthCheckHTTPPort = "query.health-check-http-port"
)
// QueryOptions holds configuration for query
type QueryOptions struct {
- // QueryPort is the port that the query service listens in on
- QueryPort int
- // QueryPrefix is the prefix of the query service api
- QueryPrefix string
- // QueryStaticAssets is the path for the static assets for the UI (https://github.com/uber/jaeger-ui)
- QueryStaticAssets string
- // QueryHealthCheckHTTPPort is the port that the health check service listens in on for http requests
- QueryHealthCheckHTTPPort int
+ // Port is the port that the query service listens in on
+ Port int
+ // Prefix is the prefix of the query service api
+ Prefix string
+ // StaticAssets is the path for the static assets for the UI (https://github.com/uber/jaeger-ui)
+ StaticAssets string
+ // UIConfig is the path to a configuration file for the UI
+ UIConfig string
+ // HealthCheckHTTPPort is the port that the health check service listens in on for http requests
+ HealthCheckHTTPPort int
}
// AddFlags adds flags for QueryOptions
@@ -44,14 +47,16 @@ func AddFlags(flagSet *flag.FlagSet) {
flagSet.Int(queryPort, 16686, "The port for the query service")
flagSet.String(queryPrefix, "api", "The prefix for the url of the query service")
flagSet.String(queryStaticFiles, "jaeger-ui-build/build/", "The path for the static assets for the UI")
+ flagSet.String(queryUIConfig, "", "The path to the UI configuration file in JSON format")
flagSet.Int(queryHealthCheckHTTPPort, 16687, "The http port for the health check service")
}
// InitFromViper initializes QueryOptions with properties from viper
func (qOpts *QueryOptions) InitFromViper(v *viper.Viper) *QueryOptions {
- qOpts.QueryPort = v.GetInt(queryPort)
- qOpts.QueryPrefix = v.GetString(queryPrefix)
- qOpts.QueryStaticAssets = v.GetString(queryStaticFiles)
- qOpts.QueryHealthCheckHTTPPort = v.GetInt(queryHealthCheckHTTPPort)
+ qOpts.Port = v.GetInt(queryPort)
+ qOpts.Prefix = v.GetString(queryPrefix)
+ qOpts.StaticAssets = v.GetString(queryStaticFiles)
+ qOpts.UIConfig = v.GetString(queryUIConfig)
+ qOpts.HealthCheckHTTPPort = v.GetInt(queryHealthCheckHTTPPort)
return qOpts
}
diff --git a/cmd/query/app/builder/builder_flags_test.go b/cmd/query/app/builder/builder_flags_test.go
index cf8731c5f2c..b008df45dd0 100644
--- a/cmd/query/app/builder/builder_flags_test.go
+++ b/cmd/query/app/builder/builder_flags_test.go
@@ -24,9 +24,15 @@ import (
func TestQueryBuilderFlags(t *testing.T) {
v, command := config.Viperize(AddFlags)
- command.ParseFlags([]string{"--query.static-files=/dev/null", "--query.prefix=api", "--query.port=80"})
+ command.ParseFlags([]string{
+ "--query.static-files=/dev/null",
+ "--query.ui-config=some.json",
+ "--query.prefix=api",
+ "--query.port=80",
+ })
qOpts := new(QueryOptions).InitFromViper(v)
- assert.Equal(t, "/dev/null", qOpts.QueryStaticAssets)
- assert.Equal(t, "api", qOpts.QueryPrefix)
- assert.Equal(t, 80, qOpts.QueryPort)
+ assert.Equal(t, "/dev/null", qOpts.StaticAssets)
+ assert.Equal(t, "some.json", qOpts.UIConfig)
+ assert.Equal(t, "api", qOpts.Prefix)
+ assert.Equal(t, 80, qOpts.Port)
}
diff --git a/cmd/query/app/fixture/index.html b/cmd/query/app/fixture/index.html
index ea05452e1c4..159ece5a20d 100644
--- a/cmd/query/app/fixture/index.html
+++ b/cmd/query/app/fixture/index.html
@@ -2,4 +2,5 @@
Test Page
+
diff --git a/cmd/query/app/fixture/ui-config-malformed.json b/cmd/query/app/fixture/ui-config-malformed.json
new file mode 100644
index 00000000000..88b6ab31830
--- /dev/null
+++ b/cmd/query/app/fixture/ui-config-malformed.json
@@ -0,0 +1 @@
+{"x" == "y"}
diff --git a/cmd/query/app/fixture/ui-config-menu.json b/cmd/query/app/fixture/ui-config-menu.json
new file mode 100644
index 00000000000..1083a021334
--- /dev/null
+++ b/cmd/query/app/fixture/ui-config-menu.json
@@ -0,0 +1,8 @@
+{
+ "menu": [
+ {
+ "label": "GitHub",
+ "url": "https://github.com/jaegertracing/jaeger"
+ }
+ ]
+}
diff --git a/cmd/query/app/fixture/ui-config.json b/cmd/query/app/fixture/ui-config.json
new file mode 100644
index 00000000000..46a5c9124a7
--- /dev/null
+++ b/cmd/query/app/fixture/ui-config.json
@@ -0,0 +1,3 @@
+{
+ "x": "y"
+}
diff --git a/cmd/query/app/fixture/ui-config.toml b/cmd/query/app/fixture/ui-config.toml
new file mode 100644
index 00000000000..cc011767a14
--- /dev/null
+++ b/cmd/query/app/fixture/ui-config.toml
@@ -0,0 +1,4 @@
+x: abcd
+z:
+ - a
+ - b
diff --git a/cmd/query/app/static_handler.go b/cmd/query/app/static_handler.go
index d92e5a49506..9c82fd2fbd8 100644
--- a/cmd/query/app/static_handler.go
+++ b/cmd/query/app/static_handler.go
@@ -15,10 +15,16 @@
package app
import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
"net/http"
+ "path/filepath"
+ "regexp"
"strings"
"github.com/gorilla/mux"
+ "github.com/pkg/errors"
)
const (
@@ -27,22 +33,67 @@ const (
var (
staticRootFiles = []string{"favicon.ico"}
+ configPattern = regexp.MustCompile("JAEGER_CONFIG *= *DEFAULT_CONFIG;")
)
// StaticAssetsHandler handles static assets
type StaticAssetsHandler struct {
staticAssetsRoot string
+ indexHTML []byte
}
// NewStaticAssetsHandler returns a StaticAssetsHandler
-func NewStaticAssetsHandler(staticAssetsRoot string) *StaticAssetsHandler {
+func NewStaticAssetsHandler(staticAssetsRoot string, uiConfig string) (*StaticAssetsHandler, error) {
if staticAssetsRoot == "" {
staticAssetsRoot = defaultStaticAssetsRoot
}
if !strings.HasSuffix(staticAssetsRoot, "/") {
staticAssetsRoot = staticAssetsRoot + "/"
}
- return &StaticAssetsHandler{staticAssetsRoot: staticAssetsRoot}
+ indexBytes, err := ioutil.ReadFile(staticAssetsRoot + "index.html")
+ if err != nil {
+ return nil, errors.Wrap(err, "Cannot read UI static assets")
+ }
+ configString := "JAEGER_CONFIG = DEFAULT_CONFIG"
+ if config, err := loadUIConfig(uiConfig); 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))
+ }
+ return &StaticAssetsHandler{
+ staticAssetsRoot: staticAssetsRoot,
+ indexHTML: configPattern.ReplaceAll(indexBytes, []byte(configString+";")),
+ }, nil
+}
+
+func loadUIConfig(uiConfig string) (map[string]interface{}, error) {
+ if uiConfig == "" {
+ return nil, nil
+ }
+ ext := filepath.Ext(uiConfig)
+ bytes, err := ioutil.ReadFile(uiConfig)
+ if err != nil {
+ return nil, errors.Wrapf(err, "Cannot read UI config file %v", uiConfig)
+ }
+
+ var c map[string]interface{}
+ var unmarshal func([]byte, interface{}) error
+
+ switch strings.ToLower(ext) {
+ case ".json":
+ unmarshal = json.Unmarshal
+ default:
+ return nil, fmt.Errorf("Unrecognized UI config file format %v", uiConfig)
+ }
+
+ if err := unmarshal(bytes, &c); err != nil {
+ return nil, errors.Wrapf(err, "Cannot parse UI config file %v", uiConfig)
+ }
+ return c, nil
}
// RegisterRoutes registers routes for this handler on the given router
@@ -57,8 +108,6 @@ func (sH *StaticAssetsHandler) RegisterRoutes(router *mux.Router) {
}
func (sH *StaticAssetsHandler) notFound(w http.ResponseWriter, r *http.Request) {
- // don't allow returning "304 Not Modified" for index.html because
- // the cached versions might have the wrong filenames for javascript assets
- delete(r.Header, "If-Modified-Since")
- http.ServeFile(w, r, sH.staticAssetsRoot+"index.html")
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.Write(sH.indexHTML)
}
diff --git a/cmd/query/app/static_handler_test.go b/cmd/query/app/static_handler_test.go
index 2ffb0e1b8d6..6324d7e6d39 100644
--- a/cmd/query/app/static_handler_test.go
+++ b/cmd/query/app/static_handler_test.go
@@ -19,6 +19,7 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
+ "strings"
"testing"
"time"
@@ -29,7 +30,8 @@ import (
func TestStaticAssetsHandler(t *testing.T) {
r := mux.NewRouter()
- handler := NewStaticAssetsHandler("fixture")
+ handler, err := NewStaticAssetsHandler("fixture", "")
+ require.NoError(t, err)
handler.RegisterRoutes(r)
server := httptest.NewServer(r)
defer server.Close()
@@ -45,13 +47,14 @@ func TestStaticAssetsHandler(t *testing.T) {
}
func TestDefaultStaticAssetsRoot(t *testing.T) {
- handler := NewStaticAssetsHandler("")
- assert.Equal(t, "jaeger-ui-build/build/", handler.staticAssetsRoot)
+ _, err := NewStaticAssetsHandler("", "")
+ assert.EqualError(t, err, "Cannot read UI static assets: open jaeger-ui-build/build/index.html: no such file or directory")
}
func TestRegisterRoutesHandler(t *testing.T) {
r := mux.NewRouter()
- handler := NewStaticAssetsHandler("fixture/")
+ handler, err := NewStaticAssetsHandler("fixture/", "")
+ require.NoError(t, err)
handler.RegisterRoutes(r)
server := httptest.NewServer(r)
defer server.Close()
@@ -72,3 +75,63 @@ func TestRegisterRoutesHandler(t *testing.T) {
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, expectedRespString, respString)
}
+
+func TestNewStaticAssetsHandlerWithConfig(t *testing.T) {
+ _, err := NewStaticAssetsHandler("fixture", "fixture/invalid-config")
+ assert.Error(t, err)
+
+ handler, err := NewStaticAssetsHandler("fixture", "fixture/ui-config.json")
+ require.NoError(t, err)
+ require.NotNil(t, handler)
+ html := string(handler.indexHTML)
+ assert.True(t, strings.Contains(html, `JAEGER_CONFIG = {"x":"y"};`), "actual: %v", html)
+}
+
+func TestLoadUIConfig(t *testing.T) {
+ type testCase struct {
+ configFile string
+ expected map[string]interface{}
+ expectedError string
+ }
+
+ run := func(description string, testCase testCase) {
+ t.Run(description, func(t *testing.T) {
+ config, err := loadUIConfig(testCase.configFile)
+ if testCase.expectedError != "" {
+ assert.EqualError(t, err, testCase.expectedError)
+ } else {
+ assert.NoError(t, err)
+ }
+ assert.EqualValues(t, testCase.expected, config)
+ })
+ }
+
+ run("no config", testCase{})
+ run("invalid 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",
+ })
+ 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"},
+ })
+ 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",
+ },
+ },
+ },
+ })
+}
diff --git a/cmd/query/main.go b/cmd/query/main.go
index 4c031d6faa0..5da985da454 100644
--- a/cmd/query/main.go
+++ b/cmd/query/main.go
@@ -61,7 +61,7 @@ func main() {
esOptions.InitFromViper(v)
queryOpts := new(builder.QueryOptions).InitFromViper(v)
- hc, err := healthcheck.Serve(http.StatusServiceUnavailable, queryOpts.QueryHealthCheckHTTPPort, logger)
+ hc, err := healthcheck.Serve(http.StatusServiceUnavailable, queryOpts.HealthCheckHTTPPort, logger)
if err != nil {
logger.Fatal("Could not start the health check server.", zap.Error(err))
}
@@ -92,21 +92,24 @@ func main() {
logger.Fatal("Failed to init storage builder", zap.Error(err))
}
- rHandler := app.NewAPIHandler(
+ apiHandler := app.NewAPIHandler(
storageBuild.SpanReader,
storageBuild.DependencyReader,
- app.HandlerOptions.Prefix(queryOpts.QueryPrefix),
+ app.HandlerOptions.Prefix(queryOpts.Prefix),
app.HandlerOptions.Logger(logger),
app.HandlerOptions.Tracer(tracer))
- sHandler := app.NewStaticAssetsHandler(queryOpts.QueryStaticAssets)
+ staticHandler, err := app.NewStaticAssetsHandler(queryOpts.StaticAssets, queryOpts.UIConfig)
+ if err != nil {
+ logger.Fatal("Could not create static assets handler", zap.Error(err))
+ }
r := mux.NewRouter()
- rHandler.RegisterRoutes(r)
- sHandler.RegisterRoutes(r)
- portStr := ":" + strconv.Itoa(queryOpts.QueryPort)
+ apiHandler.RegisterRoutes(r)
+ staticHandler.RegisterRoutes(r)
+ portStr := ":" + strconv.Itoa(queryOpts.Port)
recoveryHandler := recoveryhandler.NewRecoveryHandler(logger, true)
go func() {
- logger.Info("Starting jaeger-query HTTP server", zap.Int("port", queryOpts.QueryPort))
+ logger.Info("Starting jaeger-query HTTP server", zap.Int("port", queryOpts.Port))
if err := http.ListenAndServe(portStr, recoveryHandler(r)); err != nil {
logger.Fatal("Could not launch service", zap.Error(err))
}
diff --git a/cmd/standalone/main.go b/cmd/standalone/main.go
index 5678ffd70bb..bea4d447334 100644
--- a/cmd/standalone/main.go
+++ b/cmd/standalone/main.go
@@ -215,19 +215,22 @@ func startQuery(
logger.Fatal("Failed to initialize tracer", zap.Error(err))
}
defer closer.Close()
- rHandler := queryApp.NewAPIHandler(
+ apiHandler := queryApp.NewAPIHandler(
storageBuild.SpanReader,
storageBuild.DependencyReader,
- queryApp.HandlerOptions.Prefix(qOpts.QueryPrefix),
+ queryApp.HandlerOptions.Prefix(qOpts.Prefix),
queryApp.HandlerOptions.Logger(logger),
queryApp.HandlerOptions.Tracer(tracer))
- sHandler := queryApp.NewStaticAssetsHandler(qOpts.QueryStaticAssets)
+ staticHandler, err := queryApp.NewStaticAssetsHandler(qOpts.StaticAssets, qOpts.UIConfig)
+ if err != nil {
+ logger.Fatal("Could not create static assets handler", zap.Error(err))
+ }
r := mux.NewRouter()
- rHandler.RegisterRoutes(r)
- sHandler.RegisterRoutes(r)
- portStr := ":" + strconv.Itoa(qOpts.QueryPort)
+ apiHandler.RegisterRoutes(r)
+ staticHandler.RegisterRoutes(r)
+ portStr := ":" + strconv.Itoa(qOpts.Port)
recoveryHandler := recoveryhandler.NewRecoveryHandler(logger, true)
- logger.Info("Starting jaeger-query HTTP server", zap.Int("port", qOpts.QueryPort))
+ logger.Info("Starting jaeger-query HTTP server", zap.Int("port", qOpts.Port))
if err := http.ListenAndServe(portStr, recoveryHandler(r)); err != nil {
logger.Fatal("Could not launch jaeger-query service", zap.Error(err))
}
diff --git a/crossdock/Dockerfile b/crossdock/Dockerfile
index 5cd8eca876c..8a619570b24 100644
--- a/crossdock/Dockerfile
+++ b/crossdock/Dockerfile
@@ -5,3 +5,4 @@ EXPOSE 8080
COPY .build/scripts/* /scripts/
COPY .build/cmd/* /cmd/
+COPY .build/ui/* /ui/
diff --git a/crossdock/main.go b/crossdock/main.go
index b395766d2e0..f87cc54e097 100644
--- a/crossdock/main.go
+++ b/crossdock/main.go
@@ -131,6 +131,7 @@ func startQueryService(url string, logger *zap.Logger) services.QueryService {
forkCmd(
logger,
queryCmd,
+ "--query.static-files=/ui/",
"--cassandra.keyspace=jaeger",
"--cassandra.servers=cassandra",
)
diff --git a/crossdock/rules.mk b/crossdock/rules.mk
index cd7c1687763..e2a3ba64521 100644
--- a/crossdock/rules.mk
+++ b/crossdock/rules.mk
@@ -3,6 +3,7 @@ XDOCK_YAML=crossdock/docker-compose.yml
BUILD_DIR = crossdock/.build
CMD_DIR = $(BUILD_DIR)/cmd
SCRIPTS_DIR = $(BUILD_DIR)/scripts
+UI_DIR = $(BUILD_DIR)/ui
SCHEMA = $(SCRIPTS_DIR)/schema.cql
SCHEMA_SRC = plugin/storage/cassandra/schema/create.sh
@@ -10,6 +11,10 @@ SCHEMA_SRC = plugin/storage/cassandra/schema/create.sh
QUERY_SRC = cmd/query/query-linux
QUERY_DST = $(CMD_DIR)/jaeger-query
+# we don't actually need full UI in crossdock, so we simply provide a dummy index.html
+QUERY_INDEX_SRC = cmd/query/app/fixture/index.html
+QUERY_INDEX_DST = $(UI_DIR)/index.html
+
AGENT_SRC = cmd/agent/agent-linux
AGENT_DST = $(CMD_DIR)/jaeger-agent
@@ -25,9 +30,15 @@ $(CMD_DIR): $(BUILD_DIR)
$(SCRIPTS_DIR): $(BUILD_DIR)
mkdir -p $(SCRIPTS_DIR)
+$(UI_DIR): $(BUILD_DIR)
+ mkdir -p $(UI_DIR)
+
$(SCHEMA): $(SCRIPTS_DIR) $(SCHEMA_SRC)
MODE=test KEYSPACE=jaeger $(SCHEMA_SRC) | cat -s > $(SCHEMA)
+$(QUERY_INDEX_DST): $(UI_DIR)
+ cp $(QUERY_INDEX_SRC) $(QUERY_INDEX_DST)
+
.PHONY: crossdock-copy-bin
crossdock-copy-bin: $(CMD_DIR)
cp $(QUERY_SRC) $(QUERY_DST)
@@ -35,7 +46,7 @@ crossdock-copy-bin: $(CMD_DIR)
cp $(COLLECTOR_SRC) $(COLLECTOR_DST)
.PHONY: crossdock
-crossdock: $(SCHEMA) crossdock-copy-bin
+crossdock: $(SCHEMA) $(QUERY_INDEX_DST) crossdock-copy-bin
docker-compose -f $(XDOCK_YAML) kill test_driver go node java python
docker-compose -f $(XDOCK_YAML) rm -f test_driver
docker-compose -f $(XDOCK_YAML) build test_driver
diff --git a/jaeger-ui b/jaeger-ui
index 013d2a65fad..79d262dd12e 160000
--- a/jaeger-ui
+++ b/jaeger-ui
@@ -1 +1 @@
-Subproject commit 013d2a65fad4fa544525546749baaf08db42e442
+Subproject commit 79d262dd12e2cd64679b5d5a14aa0c7060fa85e8