history: add UI view to traces

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi
2025-02-05 22:08:38 -08:00
parent cfeea34b2d
commit f9a76355b5
16 changed files with 465 additions and 7 deletions

34
vendor/github.com/tonistiigi/jaeger-ui-rest/Dockerfile generated vendored Normal file
View File

@ -0,0 +1,34 @@
ARG NODE_VERSION=23.6
ARG ALPINE_VERSION=3.21
ARG GOLANG_VERSION=1.23
ARG JAEGERUI_VERSION=v1.66.0
FROM scratch AS jaegerui-src
ARG JAEGERUI_REPO=https://github.com/jaegertracing/jaeger-ui.git
ARG JAEGERUI_VERSION
ADD ${JAEGERUI_REPO}#${JAEGERUI_VERSION} /
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS builder
WORKDIR /work/jaeger-ui
COPY --from=jaegerui-src / .
RUN npm install
WORKDIR /work/jaeger-ui/packages/jaeger-ui
RUN NODE_ENVIRONMENT=production npm run build
# failed to find a way to avoid legacy build
RUN rm build/static/*-legacy* && rm build/static/*.png
FROM scratch AS jaegerui
COPY --from=builder /work/jaeger-ui/packages/jaeger-ui/build /
FROM alpine AS compressor
RUN --mount=target=/in,from=jaegerui <<EOT
set -ex
mkdir -p /out
cp -a /in/. /out
cd /out
find . -type f -exec sh -c 'gzip -9 -c "$1" > "$1.tmp" && mv "$1.tmp" "$1"' _ {} \;
# stop
EOT
FROM scratch AS jaegerui-compressed
COPY --from=compressor /out /

42
vendor/github.com/tonistiigi/jaeger-ui-rest/config.go generated vendored Normal file
View File

@ -0,0 +1,42 @@
package jaegerui
import (
"bytes"
"encoding/json"
"slices"
)
type Menu struct {
Label string `json:"label"`
Items []MenuItem `json:"items"`
}
type MenuItem struct {
Label string `json:"label"`
URL string `json:"url"`
}
type Config struct {
Dependencies struct {
MenuEnabled bool `json:"menuEnabled"`
} `json:"dependencies"`
Monitor struct {
MenuEnabled bool `json:"menuEnabled"`
} `json:"monitor"`
ArchiveEnabled bool `json:"archiveEnabled"`
Menu []Menu `json:"menu"`
}
func (cfg Config) Inject(name string, dt []byte) ([]byte, bool) {
if name != "index.html" {
return dt, false
}
cfgData, err := json.Marshal(cfg)
if err != nil {
return dt, false
}
dt = bytes.Replace(dt, []byte("const JAEGER_CONFIG = DEFAULT_CONFIG;"), slices.Concat([]byte(`const JAEGER_CONFIG = `), cfgData, []byte(`;`)), 1)
return dt, true
}

View File

@ -0,0 +1,117 @@
package decompress
import (
"bytes"
"compress/gzip"
"io"
"io/fs"
"path/filepath"
"sync"
)
type decompressFS struct {
fs.FS
mu sync.Mutex
data map[string][]byte
inject Injector
}
type Injector interface {
Inject(name string, dt []byte) ([]byte, bool)
}
func NewFS(fsys fs.FS, injector Injector) fs.FS {
return &decompressFS{
FS: fsys,
data: make(map[string][]byte),
inject: injector,
}
}
func (d *decompressFS) Open(name string) (fs.File, error) {
name = filepath.Clean(name)
f, err := d.FS.Open(name)
if err != nil {
return nil, err
}
d.mu.Lock()
defer d.mu.Unlock()
dt, ok := d.data[name]
if ok {
return &staticFile{
Reader: bytes.NewReader(dt),
f: f,
}, nil
}
fi, err := f.Stat()
if err != nil {
f.Close()
return nil, err
}
if fi.IsDir() {
return f, nil
}
gzReader, err := gzip.NewReader(f)
if err != nil {
f.Close()
return nil, err
}
buf := &bytes.Buffer{}
if _, err := io.Copy(buf, gzReader); err != nil {
f.Close()
return nil, err
}
dt = buf.Bytes()
if d.inject != nil {
newdt, ok := d.inject.Inject(name, dt)
if ok {
dt = newdt
}
}
d.data[name] = dt
return &staticFile{
Reader: bytes.NewReader(dt),
f: f,
}, nil
}
type staticFile struct {
*bytes.Reader
f fs.File
}
func (s *staticFile) Stat() (fs.FileInfo, error) {
fi, err := s.f.Stat()
if err != nil {
return nil, err
}
return &fileInfo{
FileInfo: fi,
size: int64(s.Len()),
}, nil
}
func (s *staticFile) Close() error {
return s.f.Close()
}
type fileInfo struct {
fs.FileInfo
size int64
}
func (f *fileInfo) Size() int64 {
return f.size
}
var _ fs.File = &staticFile{}

View File

@ -0,0 +1,3 @@
target "public" {
output = ["./public"]
}

17
vendor/github.com/tonistiigi/jaeger-ui-rest/fs.go generated vendored Normal file
View File

@ -0,0 +1,17 @@
package jaegerui
import (
"embed"
"io/fs"
"net/http"
"github.com/tonistiigi/jaeger-ui-rest/decompress"
)
//go:embed public
var staticFiles embed.FS
func FS(cfg Config) http.FileSystem {
files, _ := fs.Sub(staticFiles, "public")
return http.FS(decompress.NewFS(files, cfg))
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,3 @@
### jaeger-ui-rest
[Jaeger UI](https://github.com/jaegertracing/jaeger-ui) server with only the UI component and simple REST API for loading pregenerated JSON traces.

172
vendor/github.com/tonistiigi/jaeger-ui-rest/server.go generated vendored Normal file
View File

@ -0,0 +1,172 @@
package jaegerui
import (
"bytes"
"encoding/json"
"io"
"net"
"net/http"
"os"
"strings"
"sync"
"github.com/pkg/errors"
)
func NewServer(cfg Config) *Server {
mux := &http.ServeMux{}
s := &Server{
config: cfg,
server: &http.Server{
Handler: mux,
},
}
fsHandler := http.FileServer(FS(cfg))
mux.HandleFunc("GET /api/services", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"data": [], "total": 0}`))
})
mux.HandleFunc("GET /trace/", redirectRoot(fsHandler))
mux.HandleFunc("GET /search", redirectRoot(fsHandler))
mux.HandleFunc("POST /api/traces/", func(w http.ResponseWriter, r *http.Request) {
traceID := strings.TrimPrefix(r.URL.Path, "/api/traces/")
if traceID == "" || strings.Contains(traceID, "/") {
http.Error(w, "Invalid trace ID", http.StatusBadRequest)
return
}
handleHTTPError(s.AddTrace(traceID, r.Body), w)
})
mux.HandleFunc("GET /api/traces/", func(w http.ResponseWriter, r *http.Request) {
traceID := strings.TrimPrefix(r.URL.Path, "/api/traces/")
if traceID == "" {
qry := r.URL.Query()
ids := qry["traceID"]
if len(ids) > 0 {
dt, err := s.GetTraces(ids...)
if err != nil {
handleHTTPError(err, w)
return
}
w.Write(dt)
return
}
}
if traceID == "" || strings.Contains(traceID, "/") {
http.Error(w, "Invalid trace ID", http.StatusBadRequest)
return
}
dt, err := s.GetTraces(traceID)
if err != nil {
handleHTTPError(err, w)
return
}
w.Write(dt)
})
mux.Handle("/", fsHandler)
return s
}
type Server struct {
config Config
server *http.Server
mu sync.Mutex
traces map[string][]byte
}
func (s *Server) AddTrace(traceID string, rdr io.Reader) error {
var payload struct {
Data []struct {
TraceID string `json:"traceID"`
} `json:"data"`
}
buf := &bytes.Buffer{}
if _, err := io.Copy(buf, rdr); err != nil {
return errors.Wrapf(err, "failed to read trace data")
}
dt := buf.Bytes()
if err := json.Unmarshal(dt, &payload); err != nil {
return errors.Wrapf(err, "failed to unmarshal trace data")
}
if len(payload.Data) != 1 {
return errors.Errorf("expected 1 trace, got %d", len(payload.Data))
}
if payload.Data[0].TraceID != traceID {
return errors.Errorf("trace ID mismatch: %s != %s", payload.Data[0].TraceID, traceID)
}
s.mu.Lock()
defer s.mu.Unlock()
if s.traces == nil {
s.traces = make(map[string][]byte)
}
s.traces[traceID] = dt
return nil
}
func (s *Server) GetTraces(traceIDs ...string) ([]byte, error) {
s.mu.Lock()
defer s.mu.Unlock()
if len(traceIDs) == 0 {
return nil, errors.Errorf("trace ID is required")
}
if len(traceIDs) == 1 {
dt, ok := s.traces[traceIDs[0]]
if !ok {
return nil, errors.Wrapf(os.ErrNotExist, "trace %s not found", traceIDs[0])
}
return dt, nil
}
type payloadT struct {
Data []interface{} `json:"data"`
}
var payload payloadT
for _, traceID := range traceIDs {
dt, ok := s.traces[traceID]
if !ok {
return nil, errors.Wrapf(os.ErrNotExist, "trace %s not found", traceID)
}
var p payloadT
if err := json.Unmarshal(dt, &p); err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal trace data")
}
payload.Data = append(payload.Data, p.Data...)
}
return json.MarshalIndent(payload, "", " ")
}
func (s *Server) Serve(l net.Listener) error {
return s.server.Serve(l)
}
func redirectRoot(h http.Handler) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
r.URL.Path = "/"
h.ServeHTTP(w, r)
}
}
func handleHTTPError(err error, w http.ResponseWriter) {
switch {
case err == nil:
return
case errors.Is(err, os.ErrNotExist):
http.Error(w, err.Error(), http.StatusNotFound)
default:
http.Error(w, err.Error(), http.StatusBadRequest)
}
}