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