Skip to content

Commit

Permalink
Allow embedding custom UI config in index.html (jaegertracing#490)
Browse files Browse the repository at this point in the history
  • Loading branch information
yurishkuro authored and Isaac Hier committed Nov 1, 2017
1 parent f4d21f1 commit 6edd3f8
Show file tree
Hide file tree
Showing 16 changed files with 205 additions and 43 deletions.
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; -->
</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")
}
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

0 comments on commit 6edd3f8

Please sign in to comment.