Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow embedding custom UI config in index.html #490

Merged
merged 11 commits into from
Oct 27, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
29 changes: 17 additions & 12 deletions cmd/query/app/builder/builder_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,34 +24,39 @@ 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
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
}
14 changes: 10 additions & 4 deletions cmd/query/app/builder/builder_flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
1 change: 1 addition & 0 deletions cmd/query/app/fixture/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
<html lang="en">
<meta charset="UTF-8">
<title>Test Page</title>
<!-- JAEGER_CONFIG=DEFAULT_CONFIG; -->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change this to: "JAEGER_CONFIG = DEFAULT_CONFIG;" to be a more accurate fixture?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is it more accurate? The regexp ignores the whitespace.

</html>
1 change: 1 addition & 0 deletions cmd/query/app/fixture/ui-config-malformed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"x" == "y"}
8 changes: 8 additions & 0 deletions cmd/query/app/fixture/ui-config-menu.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"menu": [
{
"label": "GitHub",
"url": "https://github.com/jaegertracing/jaeger"
}
]
}
3 changes: 3 additions & 0 deletions cmd/query/app/fixture/ui-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"x": "y"
}
4 changes: 4 additions & 0 deletions cmd/query/app/fixture/ui-config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
x: abcd
z:
- a
- b
61 changes: 55 additions & 6 deletions cmd/query/app/static_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now it fails without UI. Before it was working. Workaround is to create empty index.html or do not include static handler if flag is empty

#493 uses query as a docker container, when xdock is executed it builds all images, Now we have to build UI which takes significant time.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's discuss on #497

}
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
Expand All @@ -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)
}
71 changes: 67 additions & 4 deletions cmd/query/app/static_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"

Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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",
},
},
},
})
}
19 changes: 11 additions & 8 deletions cmd/query/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down Expand Up @@ -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))
}
Expand Down
17 changes: 10 additions & 7 deletions cmd/standalone/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
1 change: 1 addition & 0 deletions crossdock/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ EXPOSE 8080

COPY .build/scripts/* /scripts/
COPY .build/cmd/* /cmd/
COPY .build/ui/* /ui/
1 change: 1 addition & 0 deletions crossdock/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
Expand Down
Loading