mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-05-18 00:47:48 +08:00
Merge pull request #2904 from tonistiigi/history-command-trace
Add history trace command
This commit is contained in:
commit
350d3f0f4b
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@ -174,6 +174,11 @@ jobs:
|
||||
env:
|
||||
SKIP_INTEGRATION_TESTS: 1
|
||||
steps:
|
||||
-
|
||||
name: Setup Git config
|
||||
run: |
|
||||
git config --global core.autocrlf false
|
||||
git config --global core.eol lf
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
@ -24,6 +24,7 @@ func RootCmd(rootcmd *cobra.Command, dockerCli command.Cli, opts RootOptions) *c
|
||||
logsCmd(dockerCli, opts),
|
||||
inspectCmd(dockerCli, opts),
|
||||
openCmd(dockerCli, opts),
|
||||
traceCmd(dockerCli, opts),
|
||||
)
|
||||
|
||||
return cmd
|
||||
|
260
commands/history/trace.go
Normal file
260
commands/history/trace.go
Normal file
@ -0,0 +1,260 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/containerd/containerd/v2/core/content/proxy"
|
||||
"github.com/docker/buildx/builder"
|
||||
"github.com/docker/buildx/util/cobrautil/completion"
|
||||
"github.com/docker/buildx/util/otelutil"
|
||||
"github.com/docker/buildx/util/otelutil/jaeger"
|
||||
"github.com/docker/cli/cli/command"
|
||||
controlapi "github.com/moby/buildkit/api/services/control"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/browser"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
jaegerui "github.com/tonistiigi/jaeger-ui-rest"
|
||||
)
|
||||
|
||||
type traceOptions struct {
|
||||
builder string
|
||||
ref string
|
||||
addr string
|
||||
compare string
|
||||
}
|
||||
|
||||
func loadTrace(ctx context.Context, ref string, nodes []builder.Node) (string, []byte, error) {
|
||||
var offset *int
|
||||
if strings.HasPrefix(ref, "^") {
|
||||
off, err := strconv.Atoi(ref[1:])
|
||||
if err != nil {
|
||||
return "", nil, errors.Wrapf(err, "invalid offset %q", ref)
|
||||
}
|
||||
offset = &off
|
||||
ref = ""
|
||||
}
|
||||
|
||||
recs, err := queryRecords(ctx, ref, nodes)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
var rec *historyRecord
|
||||
|
||||
if ref == "" {
|
||||
slices.SortFunc(recs, func(a, b historyRecord) int {
|
||||
return b.CreatedAt.AsTime().Compare(a.CreatedAt.AsTime())
|
||||
})
|
||||
for _, r := range recs {
|
||||
if r.CompletedAt != nil {
|
||||
if offset != nil {
|
||||
if *offset > 0 {
|
||||
*offset--
|
||||
continue
|
||||
}
|
||||
}
|
||||
rec = &r
|
||||
break
|
||||
}
|
||||
}
|
||||
if offset != nil && *offset > 0 {
|
||||
return "", nil, errors.Errorf("no completed build found with offset %d", *offset)
|
||||
}
|
||||
} else {
|
||||
rec = &recs[0]
|
||||
}
|
||||
if rec == nil {
|
||||
if ref == "" {
|
||||
return "", nil, errors.New("no records found")
|
||||
}
|
||||
return "", nil, errors.Errorf("no record found for ref %q", ref)
|
||||
}
|
||||
|
||||
if rec.CompletedAt == nil {
|
||||
return "", nil, errors.Errorf("build %q is not completed, only completed builds can be traced", rec.Ref)
|
||||
}
|
||||
|
||||
if rec.Trace == nil {
|
||||
// build is complete but no trace yet. try to finalize the trace
|
||||
time.Sleep(1 * time.Second) // give some extra time for last parts of trace to be written
|
||||
|
||||
c, err := rec.node.Driver.Client(ctx)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
_, err = c.ControlClient().UpdateBuildHistory(ctx, &controlapi.UpdateBuildHistoryRequest{
|
||||
Ref: rec.Ref,
|
||||
Finalize: true,
|
||||
})
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
recs, err := queryRecords(ctx, rec.Ref, []builder.Node{*rec.node})
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
if len(recs) == 0 {
|
||||
return "", nil, errors.Errorf("build record %q was deleted", rec.Ref)
|
||||
}
|
||||
|
||||
rec = &recs[0]
|
||||
if rec.Trace == nil {
|
||||
return "", nil, errors.Errorf("build record %q is missing a trace", rec.Ref)
|
||||
}
|
||||
}
|
||||
|
||||
c, err := rec.node.Driver.Client(ctx)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
store := proxy.NewContentStore(c.ContentClient())
|
||||
|
||||
ra, err := store.ReaderAt(ctx, ocispecs.Descriptor{
|
||||
Digest: digest.Digest(rec.Trace.Digest),
|
||||
MediaType: rec.Trace.MediaType,
|
||||
Size: rec.Trace.Size,
|
||||
})
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
spans, err := otelutil.ParseSpanStubs(io.NewSectionReader(ra, 0, ra.Size()))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
wrapper := struct {
|
||||
Data []jaeger.Trace `json:"data"`
|
||||
}{
|
||||
Data: spans.JaegerData().Data,
|
||||
}
|
||||
|
||||
if len(wrapper.Data) == 0 {
|
||||
return "", nil, errors.New("no trace data")
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(wrapper); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return string(wrapper.Data[0].TraceID), buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func runTrace(ctx context.Context, dockerCli command.Cli, opts traceOptions) error {
|
||||
b, err := builder.New(dockerCli, builder.WithName(opts.builder))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nodes, err := b.LoadNodes(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node.Err != nil {
|
||||
return node.Err
|
||||
}
|
||||
}
|
||||
|
||||
traceID, data, err := loadTrace(ctx, opts.ref, nodes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv := jaegerui.NewServer(jaegerui.Config{})
|
||||
if err := srv.AddTrace(traceID, bytes.NewReader(data)); err != nil {
|
||||
return err
|
||||
}
|
||||
url := "/trace/" + traceID
|
||||
|
||||
if opts.compare != "" {
|
||||
traceIDcomp, data, err := loadTrace(ctx, opts.compare, nodes)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to load trace for %s", opts.compare)
|
||||
}
|
||||
if err := srv.AddTrace(traceIDcomp, bytes.NewReader(data)); err != nil {
|
||||
return err
|
||||
}
|
||||
url = "/trace/" + traceIDcomp + "..." + traceID
|
||||
}
|
||||
|
||||
var term bool
|
||||
if _, err := console.ConsoleFromFile(os.Stdout); err == nil {
|
||||
term = true
|
||||
}
|
||||
|
||||
if !term && opts.compare == "" {
|
||||
fmt.Fprintln(dockerCli.Out(), string(data))
|
||||
return nil
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", opts.addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
browser.OpenURL(url)
|
||||
}()
|
||||
|
||||
url = "http://" + ln.Addr().String() + url
|
||||
fmt.Fprintf(dockerCli.Err(), "Trace available at %s\n", url)
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
ln.Close()
|
||||
}()
|
||||
|
||||
err = srv.Serve(ln)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func traceCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
|
||||
var options traceOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "trace [OPTIONS] [REF]",
|
||||
Short: "Show the OpenTelemetry trace of a build record",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 0 {
|
||||
options.ref = args[0]
|
||||
}
|
||||
options.builder = *rootOpts.Builder
|
||||
return runTrace(cmd.Context(), dockerCli, options)
|
||||
},
|
||||
ValidArgsFunction: completion.Disable,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVar(&options.addr, "addr", "127.0.0.1:0", "Address to bind the UI server")
|
||||
flags.StringVar(&options.compare, "compare", "", "Compare with another build reference")
|
||||
|
||||
return cmd
|
||||
}
|
@ -5,13 +5,14 @@ Commands to work on build records
|
||||
|
||||
### Subcommands
|
||||
|
||||
| Name | Description |
|
||||
|:---------------------------------------|:-------------------------------|
|
||||
| [`inspect`](buildx_history_inspect.md) | Inspect a build |
|
||||
| [`logs`](buildx_history_logs.md) | Print the logs of a build |
|
||||
| [`ls`](buildx_history_ls.md) | List build records |
|
||||
| [`open`](buildx_history_open.md) | Open a build in Docker Desktop |
|
||||
| [`rm`](buildx_history_rm.md) | Remove build records |
|
||||
| Name | Description |
|
||||
|:---------------------------------------|:-----------------------------------------------|
|
||||
| [`inspect`](buildx_history_inspect.md) | Inspect a build |
|
||||
| [`logs`](buildx_history_logs.md) | Print the logs of a build |
|
||||
| [`ls`](buildx_history_ls.md) | List build records |
|
||||
| [`open`](buildx_history_open.md) | Open a build in Docker Desktop |
|
||||
| [`rm`](buildx_history_rm.md) | Remove build records |
|
||||
| [`trace`](buildx_history_trace.md) | Show the OpenTelemetry trace of a build record |
|
||||
|
||||
|
||||
### Options
|
||||
|
17
docs/reference/buildx_history_trace.md
Normal file
17
docs/reference/buildx_history_trace.md
Normal file
@ -0,0 +1,17 @@
|
||||
# docker buildx history trace
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
Show the OpenTelemetry trace of a build record
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:----------------|:---------|:--------------|:-----------------------------------------|
|
||||
| `--addr` | `string` | `127.0.0.1:0` | Address to bind the UI server |
|
||||
| `--builder` | `string` | | Override the configured builder instance |
|
||||
| `--compare` | `string` | | Compare with another build reference |
|
||||
| `-D`, `--debug` | `bool` | | Enable debug logging |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
2
go.mod
2
go.mod
@ -46,8 +46,10 @@ require (
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tonistiigi/fsutil v0.0.0-20250113203817-b14e27f4135a
|
||||
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4
|
||||
github.com/tonistiigi/jaeger-ui-rest v0.0.0-20250211190051-7d4944a45bb6
|
||||
github.com/zclconf/go-cty v1.16.0
|
||||
go.opentelemetry.io/otel v1.31.0
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0
|
||||
go.opentelemetry.io/otel/metric v1.31.0
|
||||
go.opentelemetry.io/otel/sdk v1.31.0
|
||||
go.opentelemetry.io/otel/trace v1.31.0
|
||||
|
4
go.sum
4
go.sum
@ -447,6 +447,8 @@ github.com/tonistiigi/fsutil v0.0.0-20250113203817-b14e27f4135a h1:EfGw4G0x/8qXW
|
||||
github.com/tonistiigi/fsutil v0.0.0-20250113203817-b14e27f4135a/go.mod h1:Dl/9oEjK7IqnjAm21Okx/XIxUCFJzvh+XdVHUlBwXTw=
|
||||
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 h1:7I5c2Ig/5FgqkYOh/N87NzoyI9U15qUPXhDD8uCupv8=
|
||||
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE=
|
||||
github.com/tonistiigi/jaeger-ui-rest v0.0.0-20250211190051-7d4944a45bb6 h1:RT/a0RvdX84iwtOrUK45+wjcNpaG+hS7n7XFYqj4axg=
|
||||
github.com/tonistiigi/jaeger-ui-rest v0.0.0-20250211190051-7d4944a45bb6/go.mod h1:3Ez1Paeg+0Ghu3KwpEGC1HgZ4CHDlg+Ez/5Baeomk54=
|
||||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0=
|
||||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk=
|
||||
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw=
|
||||
@ -490,6 +492,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 h1:FFeLy
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0/go.mod h1:TMu73/k1CP8nBUpDLc71Wj/Kf7ZS9FK5b53VapRsP9o=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0 h1:UGZ1QwZWY67Z6BmckTU+9Rxn04m2bD3gD6Mk0OIOCPk=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0/go.mod h1:fcwWuDuaObkkChiDlhEpSq9+X1C0omv+s5mBtToAQ64=
|
||||
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
|
||||
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
|
||||
go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
|
||||
|
11125
util/otelutil/fixtures/bktraces.json
Normal file
11125
util/otelutil/fixtures/bktraces.json
Normal file
File diff suppressed because it is too large
Load Diff
9542
util/otelutil/fixtures/jaeger.json
Normal file
9542
util/otelutil/fixtures/jaeger.json
Normal file
File diff suppressed because it is too large
Load Diff
11127
util/otelutil/fixtures/otlp.json
Normal file
11127
util/otelutil/fixtures/otlp.json
Normal file
File diff suppressed because it is too large
Load Diff
45
util/otelutil/jaeger.go
Normal file
45
util/otelutil/jaeger.go
Normal file
@ -0,0 +1,45 @@
|
||||
package otelutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/buildx/util/otelutil/jaeger"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
)
|
||||
|
||||
type JaegerData struct {
|
||||
Data []jaeger.Trace `json:"data"`
|
||||
}
|
||||
|
||||
// JaegerData return Jaeger data compatible with ui import feature.
|
||||
// https://github.com/jaegertracing/jaeger-ui/issues/381#issuecomment-494150826
|
||||
func (s Spans) JaegerData() JaegerData {
|
||||
roSpans := s.ReadOnlySpans()
|
||||
|
||||
// fetch default service.name from default resource for backup
|
||||
var defaultServiceName string
|
||||
defaultResource := resource.Default()
|
||||
if value, exists := defaultResource.Set().Value(attribute.Key("service.name")); exists {
|
||||
defaultServiceName = value.AsString()
|
||||
}
|
||||
|
||||
data := jaeger.Trace{
|
||||
TraceID: jaeger.TraceID(roSpans[0].SpanContext().TraceID().String()),
|
||||
Processes: make(map[jaeger.ProcessID]jaeger.Process),
|
||||
Spans: []jaeger.Span{},
|
||||
}
|
||||
for i := range roSpans {
|
||||
ss := roSpans[i]
|
||||
pid := jaeger.ProcessID(fmt.Sprintf("p%d", i))
|
||||
data.Processes[pid] = jaeger.ResourceToProcess(ss.Resource(), defaultServiceName)
|
||||
span := jaeger.ConvertSpan(ss)
|
||||
span.Process = nil
|
||||
span.ProcessID = pid
|
||||
data.Spans = append(data.Spans, span)
|
||||
}
|
||||
|
||||
return JaegerData{
|
||||
Data: []jaeger.Trace{data},
|
||||
}
|
||||
}
|
224
util/otelutil/jaeger/convert.go
Normal file
224
util/otelutil/jaeger/convert.go
Normal file
@ -0,0 +1,224 @@
|
||||
package jaeger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
tracesdk "go.opentelemetry.io/otel/sdk/trace"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
const (
|
||||
keyInstrumentationLibraryName = "otel.library.name"
|
||||
keyInstrumentationLibraryVersion = "otel.library.version"
|
||||
keyError = "error"
|
||||
keySpanKind = "span.kind"
|
||||
keyStatusCode = "otel.status_code"
|
||||
keyStatusMessage = "otel.status_description"
|
||||
keyDroppedAttributeCount = "otel.event.dropped_attributes_count"
|
||||
keyEventName = "event"
|
||||
)
|
||||
|
||||
func ResourceToProcess(res *resource.Resource, defaultServiceName string) Process {
|
||||
var process Process
|
||||
var serviceName attribute.KeyValue
|
||||
if res != nil {
|
||||
for iter := res.Iter(); iter.Next(); {
|
||||
if iter.Attribute().Key == attribute.Key("service.name") {
|
||||
serviceName = iter.Attribute()
|
||||
// Don't convert service.name into tag.
|
||||
continue
|
||||
}
|
||||
if tag := keyValueToJaegerTag(iter.Attribute()); tag != nil {
|
||||
process.Tags = append(process.Tags, *tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no service.name is contained in a Span's Resource,
|
||||
// that field MUST be populated from the default Resource.
|
||||
if serviceName.Value.AsString() == "" {
|
||||
serviceName = attribute.Key("service.version").String(defaultServiceName)
|
||||
}
|
||||
process.ServiceName = serviceName.Value.AsString()
|
||||
|
||||
return process
|
||||
}
|
||||
|
||||
func ConvertSpan(ss tracesdk.ReadOnlySpan) Span {
|
||||
attr := ss.Attributes()
|
||||
tags := make([]KeyValue, 0, len(attr))
|
||||
for _, kv := range attr {
|
||||
tag := keyValueToJaegerTag(kv)
|
||||
if tag != nil {
|
||||
tags = append(tags, *tag)
|
||||
}
|
||||
}
|
||||
|
||||
if is := ss.InstrumentationScope(); is.Name != "" {
|
||||
tags = append(tags, getStringTag(keyInstrumentationLibraryName, is.Name))
|
||||
if is.Version != "" {
|
||||
tags = append(tags, getStringTag(keyInstrumentationLibraryVersion, is.Version))
|
||||
}
|
||||
}
|
||||
|
||||
if ss.SpanKind() != trace.SpanKindInternal {
|
||||
tags = append(tags,
|
||||
getStringTag(keySpanKind, ss.SpanKind().String()),
|
||||
)
|
||||
}
|
||||
|
||||
if ss.Status().Code != codes.Unset {
|
||||
switch ss.Status().Code {
|
||||
case codes.Ok:
|
||||
tags = append(tags, getStringTag(keyStatusCode, "OK"))
|
||||
case codes.Error:
|
||||
tags = append(tags, getBoolTag(keyError, true))
|
||||
tags = append(tags, getStringTag(keyStatusCode, "ERROR"))
|
||||
}
|
||||
if ss.Status().Description != "" {
|
||||
tags = append(tags, getStringTag(keyStatusMessage, ss.Status().Description))
|
||||
}
|
||||
}
|
||||
|
||||
var logs []Log
|
||||
for _, a := range ss.Events() {
|
||||
nTags := len(a.Attributes)
|
||||
if a.Name != "" {
|
||||
nTags++
|
||||
}
|
||||
if a.DroppedAttributeCount != 0 {
|
||||
nTags++
|
||||
}
|
||||
fields := make([]KeyValue, 0, nTags)
|
||||
if a.Name != "" {
|
||||
// If an event contains an attribute with the same key, it needs
|
||||
// to be given precedence and overwrite this.
|
||||
fields = append(fields, getStringTag(keyEventName, a.Name))
|
||||
}
|
||||
for _, kv := range a.Attributes {
|
||||
tag := keyValueToJaegerTag(kv)
|
||||
if tag != nil {
|
||||
fields = append(fields, *tag)
|
||||
}
|
||||
}
|
||||
if a.DroppedAttributeCount != 0 {
|
||||
fields = append(fields, getInt64Tag(keyDroppedAttributeCount, int64(a.DroppedAttributeCount)))
|
||||
}
|
||||
logs = append(logs, Log{
|
||||
Timestamp: timeAsEpochMicroseconds(a.Time),
|
||||
Fields: fields,
|
||||
})
|
||||
}
|
||||
|
||||
var refs []Reference
|
||||
for _, link := range ss.Links() {
|
||||
refs = append(refs, Reference{
|
||||
RefType: FollowsFrom,
|
||||
TraceID: TraceID(link.SpanContext.TraceID().String()),
|
||||
SpanID: SpanID(link.SpanContext.SpanID().String()),
|
||||
})
|
||||
}
|
||||
refs = append(refs, Reference{
|
||||
RefType: ChildOf,
|
||||
TraceID: TraceID(ss.Parent().TraceID().String()),
|
||||
SpanID: SpanID(ss.Parent().SpanID().String()),
|
||||
})
|
||||
|
||||
return Span{
|
||||
TraceID: TraceID(ss.SpanContext().TraceID().String()),
|
||||
SpanID: SpanID(ss.SpanContext().SpanID().String()),
|
||||
Flags: uint32(ss.SpanContext().TraceFlags()),
|
||||
OperationName: ss.Name(),
|
||||
References: refs,
|
||||
StartTime: timeAsEpochMicroseconds(ss.StartTime()),
|
||||
Duration: durationAsMicroseconds(ss.EndTime().Sub(ss.StartTime())),
|
||||
Tags: tags,
|
||||
Logs: logs,
|
||||
}
|
||||
}
|
||||
|
||||
func keyValueToJaegerTag(keyValue attribute.KeyValue) *KeyValue {
|
||||
var tag *KeyValue
|
||||
switch keyValue.Value.Type() {
|
||||
case attribute.STRING:
|
||||
s := keyValue.Value.AsString()
|
||||
tag = &KeyValue{
|
||||
Key: string(keyValue.Key),
|
||||
Type: StringType,
|
||||
Value: s,
|
||||
}
|
||||
case attribute.BOOL:
|
||||
b := keyValue.Value.AsBool()
|
||||
tag = &KeyValue{
|
||||
Key: string(keyValue.Key),
|
||||
Type: BoolType,
|
||||
Value: b,
|
||||
}
|
||||
case attribute.INT64:
|
||||
i := keyValue.Value.AsInt64()
|
||||
tag = &KeyValue{
|
||||
Key: string(keyValue.Key),
|
||||
Type: Int64Type,
|
||||
Value: i,
|
||||
}
|
||||
case attribute.FLOAT64:
|
||||
f := keyValue.Value.AsFloat64()
|
||||
tag = &KeyValue{
|
||||
Key: string(keyValue.Key),
|
||||
Type: Float64Type,
|
||||
Value: f,
|
||||
}
|
||||
case attribute.BOOLSLICE,
|
||||
attribute.INT64SLICE,
|
||||
attribute.FLOAT64SLICE,
|
||||
attribute.STRINGSLICE:
|
||||
data, _ := json.Marshal(keyValue.Value.AsInterface())
|
||||
a := (string)(data)
|
||||
tag = &KeyValue{
|
||||
Key: string(keyValue.Key),
|
||||
Type: StringType,
|
||||
Value: a,
|
||||
}
|
||||
}
|
||||
return tag
|
||||
}
|
||||
|
||||
func getInt64Tag(k string, i int64) KeyValue {
|
||||
return KeyValue{
|
||||
Key: k,
|
||||
Type: Int64Type,
|
||||
Value: i,
|
||||
}
|
||||
}
|
||||
|
||||
func getStringTag(k, s string) KeyValue {
|
||||
return KeyValue{
|
||||
Key: k,
|
||||
Type: StringType,
|
||||
Value: s,
|
||||
}
|
||||
}
|
||||
|
||||
func getBoolTag(k string, b bool) KeyValue {
|
||||
return KeyValue{
|
||||
Key: k,
|
||||
Type: BoolType,
|
||||
Value: b,
|
||||
}
|
||||
}
|
||||
|
||||
// timeAsEpochMicroseconds converts time.Time to microseconds since epoch,
|
||||
// which is the format the StartTime field is stored in the Span.
|
||||
func timeAsEpochMicroseconds(t time.Time) uint64 {
|
||||
return uint64(t.UnixNano() / 1000)
|
||||
}
|
||||
|
||||
// durationAsMicroseconds converts time.Duration to microseconds,
|
||||
// which is the format the Duration field is stored in the Span.
|
||||
func durationAsMicroseconds(d time.Duration) uint64 {
|
||||
return uint64(d.Nanoseconds() / 1000)
|
||||
}
|
102
util/otelutil/jaeger/model.go
Normal file
102
util/otelutil/jaeger/model.go
Normal file
@ -0,0 +1,102 @@
|
||||
package jaeger
|
||||
|
||||
// ReferenceType is the reference type of one span to another
|
||||
type ReferenceType string
|
||||
|
||||
// TraceID is the shared trace ID of all spans in the trace.
|
||||
type TraceID string
|
||||
|
||||
// SpanID is the id of a span
|
||||
type SpanID string
|
||||
|
||||
// ProcessID is a hashed value of the Process struct that is unique within the trace.
|
||||
type ProcessID string
|
||||
|
||||
// ValueType is the type of a value stored in KeyValue struct.
|
||||
type ValueType string
|
||||
|
||||
const (
|
||||
// ChildOf means a span is the child of another span
|
||||
ChildOf ReferenceType = "CHILD_OF"
|
||||
// FollowsFrom means a span follows from another span
|
||||
FollowsFrom ReferenceType = "FOLLOWS_FROM"
|
||||
|
||||
// StringType indicates a string value stored in KeyValue
|
||||
StringType ValueType = "string"
|
||||
// BoolType indicates a Boolean value stored in KeyValue
|
||||
BoolType ValueType = "bool"
|
||||
// Int64Type indicates a 64bit signed integer value stored in KeyValue
|
||||
Int64Type ValueType = "int64"
|
||||
// Float64Type indicates a 64bit float value stored in KeyValue
|
||||
Float64Type ValueType = "float64"
|
||||
// BinaryType indicates an arbitrary byte array stored in KeyValue
|
||||
BinaryType ValueType = "binary"
|
||||
)
|
||||
|
||||
// Trace is a list of spans
|
||||
type Trace struct {
|
||||
TraceID TraceID `json:"traceID"`
|
||||
Spans []Span `json:"spans"`
|
||||
Processes map[ProcessID]Process `json:"processes"`
|
||||
Warnings []string `json:"warnings"`
|
||||
}
|
||||
|
||||
// Span is a span denoting a piece of work in some infrastructure
|
||||
// When converting to UI model, ParentSpanID and Process should be dereferenced into
|
||||
// References and ProcessID, respectively.
|
||||
// When converting to ES model, ProcessID and Warnings should be omitted. Even if
|
||||
// included, ES with dynamic settings off will automatically ignore unneeded fields.
|
||||
type Span struct {
|
||||
TraceID TraceID `json:"traceID"`
|
||||
SpanID SpanID `json:"spanID"`
|
||||
ParentSpanID SpanID `json:"parentSpanID,omitempty"` // deprecated
|
||||
Flags uint32 `json:"flags,omitempty"`
|
||||
OperationName string `json:"operationName"`
|
||||
References []Reference `json:"references"`
|
||||
StartTime uint64 `json:"startTime"` // microseconds since Unix epoch
|
||||
Duration uint64 `json:"duration"` // microseconds
|
||||
Tags []KeyValue `json:"tags"`
|
||||
Logs []Log `json:"logs"`
|
||||
ProcessID ProcessID `json:"processID,omitempty"`
|
||||
Process *Process `json:"process,omitempty"`
|
||||
Warnings []string `json:"warnings"`
|
||||
}
|
||||
|
||||
// Reference is a reference from one span to another
|
||||
type Reference struct {
|
||||
RefType ReferenceType `json:"refType"`
|
||||
TraceID TraceID `json:"traceID"`
|
||||
SpanID SpanID `json:"spanID"`
|
||||
}
|
||||
|
||||
// Process is the process emitting a set of spans
|
||||
type Process struct {
|
||||
ServiceName string `json:"serviceName"`
|
||||
Tags []KeyValue `json:"tags"`
|
||||
}
|
||||
|
||||
// Log is a log emitted in a span
|
||||
type Log struct {
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
Fields []KeyValue `json:"fields"`
|
||||
}
|
||||
|
||||
// KeyValue is a key-value pair with typed value.
|
||||
type KeyValue struct {
|
||||
Key string `json:"key"`
|
||||
Type ValueType `json:"type,omitempty"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
// DependencyLink shows dependencies between services
|
||||
type DependencyLink struct {
|
||||
Parent string `json:"parent"`
|
||||
Child string `json:"child"`
|
||||
CallCount uint64 `json:"callCount"`
|
||||
}
|
||||
|
||||
// Operation defines the data in the operation response when query operation by service and span kind
|
||||
type Operation struct {
|
||||
Name string `json:"name"`
|
||||
SpanKind string `json:"spanKind"`
|
||||
}
|
27
util/otelutil/jaeger_test.go
Normal file
27
util/otelutil/jaeger_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
package otelutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const jaegerFixture = "./fixtures/jaeger.json"
|
||||
|
||||
func TestJaegerData(t *testing.T) {
|
||||
dt, err := os.ReadFile(bktracesFixture)
|
||||
require.NoError(t, err)
|
||||
|
||||
spanStubs, err := ParseSpanStubs(bytes.NewReader(dt))
|
||||
require.NoError(t, err)
|
||||
|
||||
trace := spanStubs.JaegerData()
|
||||
dtJaegerTrace, err := json.MarshalIndent(trace, "", " ")
|
||||
require.NoError(t, err)
|
||||
dtJaeger, err := os.ReadFile(jaegerFixture)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, string(dtJaeger), string(dtJaegerTrace))
|
||||
}
|
491
util/otelutil/span.go
Normal file
491
util/otelutil/span.go
Normal file
@ -0,0 +1,491 @@
|
||||
package otelutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/sdk/instrumentation"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
tracesdk "go.opentelemetry.io/otel/sdk/trace"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// Span is a type similar to otel's SpanStub, but with the correct types needed
|
||||
// for handle marshaling and unmarshalling.
|
||||
type Span struct {
|
||||
// Name is the name of a specific span
|
||||
Name string
|
||||
// SpanContext is the unique SpanContext that identifies the span
|
||||
SpanContext trace.SpanContext
|
||||
// Parten is the unique SpanContext that identifies the parent of the span.
|
||||
// If the span has no parent, this span context will be invalid.
|
||||
Parent trace.SpanContext
|
||||
// SpanKind is the role the span plays in a Trace
|
||||
SpanKind trace.SpanKind
|
||||
// StartTime is the time the span started recording
|
||||
StartTime time.Time
|
||||
// EndTime returns the time the span stopped recording
|
||||
EndTime time.Time
|
||||
// Attributes are the defining attributes of a span
|
||||
Attributes []attribute.KeyValue
|
||||
// Events are all the events that occurred within the span
|
||||
Events []tracesdk.Event
|
||||
// Links are all the links the span has to other spans
|
||||
Links []tracesdk.Link
|
||||
// Status is that span status
|
||||
Status tracesdk.Status
|
||||
// DroppedAttributes is the number of attributes dropped by the span due to
|
||||
// a limit being reached
|
||||
DroppedAttributes int
|
||||
// DroppedEvents is the number of attributes dropped by the span due to a
|
||||
// limit being reached
|
||||
DroppedEvents int
|
||||
// DroppedLinks is the number of links dropped by the span due to a limit
|
||||
// being reached
|
||||
DroppedLinks int
|
||||
// ChildSpanCount is the count of spans that consider the span a direct
|
||||
// parent
|
||||
ChildSpanCount int
|
||||
// Resource is the information about the entity that produced the span
|
||||
// We have to change this type from the otel type to make this struct
|
||||
// marshallable
|
||||
Resource []attribute.KeyValue
|
||||
// InstrumentationLibrary is information about the library that produced
|
||||
// the span
|
||||
//nolint:staticcheck
|
||||
InstrumentationLibrary instrumentation.Library
|
||||
}
|
||||
|
||||
type Spans []Span
|
||||
|
||||
// Len return the length of the Spans.
|
||||
func (s Spans) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
// ReadOnlySpans return a list of tracesdk.ReadOnlySpan from span stubs.
|
||||
func (s Spans) ReadOnlySpans() []tracesdk.ReadOnlySpan {
|
||||
roSpans := make([]tracesdk.ReadOnlySpan, len(s))
|
||||
for i := range s {
|
||||
roSpans[i] = s[i].Snapshot()
|
||||
}
|
||||
return roSpans
|
||||
}
|
||||
|
||||
// ParseSpanStubs parses BuildKit trace data into a list of SpanStubs.
|
||||
func ParseSpanStubs(rdr io.Reader) (Spans, error) {
|
||||
var spanStubs []Span
|
||||
decoder := json.NewDecoder(rdr)
|
||||
for {
|
||||
var span Span
|
||||
if err := decoder.Decode(&span); err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrapf(err, "error decoding JSON")
|
||||
}
|
||||
spanStubs = append(spanStubs, span)
|
||||
}
|
||||
return spanStubs, nil
|
||||
}
|
||||
|
||||
// spanData is data that we need to unmarshal in custom ways.
|
||||
type spanData struct {
|
||||
Name string
|
||||
SpanContext spanContext
|
||||
Parent spanContext
|
||||
SpanKind trace.SpanKind
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
Attributes []keyValue
|
||||
Events []event
|
||||
Links []link
|
||||
Status tracesdk.Status
|
||||
DroppedAttributes int
|
||||
DroppedEvents int
|
||||
DroppedLinks int
|
||||
ChildSpanCount int
|
||||
Resource []keyValue // change this type from the otel type to make this struct marshallable
|
||||
//nolint:staticcheck
|
||||
InstrumentationLibrary instrumentation.Library
|
||||
}
|
||||
|
||||
// spanContext is a custom type used to unmarshal otel SpanContext correctly.
|
||||
type spanContext struct {
|
||||
TraceID string
|
||||
SpanID string
|
||||
TraceFlags string
|
||||
TraceState string // TODO: implement, currently dropped
|
||||
Remote bool
|
||||
}
|
||||
|
||||
// event is a custom type used to unmarshal otel Event correctly.
|
||||
type event struct {
|
||||
Name string
|
||||
Attributes []keyValue
|
||||
DroppedAttributeCount int
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
// link is a custom type used to unmarshal otel Link correctly.
|
||||
type link struct {
|
||||
SpanContext spanContext
|
||||
Attributes []keyValue
|
||||
DroppedAttributeCount int
|
||||
}
|
||||
|
||||
// keyValue is a custom type used to unmarshal otel KeyValue correctly.
|
||||
type keyValue struct {
|
||||
Key string
|
||||
Value value
|
||||
}
|
||||
|
||||
// value is a custom type used to unmarshal otel Value correctly.
|
||||
type value struct {
|
||||
Type string
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler for Span which allows correctly
|
||||
// retrieving attribute.KeyValue values.
|
||||
func (s *Span) UnmarshalJSON(data []byte) error {
|
||||
var sd spanData
|
||||
if err := json.NewDecoder(bytes.NewReader(data)).Decode(&sd); err != nil {
|
||||
return errors.Wrap(err, "unable to decode to spanData")
|
||||
}
|
||||
|
||||
s.Name = sd.Name
|
||||
s.SpanKind = sd.SpanKind
|
||||
s.StartTime = sd.StartTime
|
||||
s.EndTime = sd.EndTime
|
||||
s.Status = sd.Status
|
||||
s.DroppedAttributes = sd.DroppedAttributes
|
||||
s.DroppedEvents = sd.DroppedEvents
|
||||
s.DroppedLinks = sd.DroppedLinks
|
||||
s.ChildSpanCount = sd.ChildSpanCount
|
||||
s.InstrumentationLibrary = sd.InstrumentationLibrary
|
||||
|
||||
spanCtx, err := sd.SpanContext.asTraceSpanContext()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to decode spanCtx")
|
||||
}
|
||||
s.SpanContext = spanCtx
|
||||
|
||||
parent, err := sd.Parent.asTraceSpanContext()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to decode parent")
|
||||
}
|
||||
s.Parent = parent
|
||||
|
||||
var attributes []attribute.KeyValue
|
||||
for _, a := range sd.Attributes {
|
||||
kv, err := a.asAttributeKeyValue()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to decode attribute (%s)", a.Key)
|
||||
}
|
||||
attributes = append(attributes, kv)
|
||||
}
|
||||
s.Attributes = attributes
|
||||
|
||||
var events []tracesdk.Event
|
||||
for _, e := range sd.Events {
|
||||
var eventAttributes []attribute.KeyValue
|
||||
for _, a := range e.Attributes {
|
||||
kv, err := a.asAttributeKeyValue()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to decode event attribute (%s)", a.Key)
|
||||
}
|
||||
eventAttributes = append(eventAttributes, kv)
|
||||
}
|
||||
events = append(events, tracesdk.Event{
|
||||
Name: e.Name,
|
||||
Attributes: eventAttributes,
|
||||
DroppedAttributeCount: e.DroppedAttributeCount,
|
||||
Time: e.Time,
|
||||
})
|
||||
}
|
||||
s.Events = events
|
||||
|
||||
var links []tracesdk.Link
|
||||
for _, l := range sd.Links {
|
||||
linkSpanCtx, err := l.SpanContext.asTraceSpanContext()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to decode linkSpanCtx")
|
||||
}
|
||||
var linkAttributes []attribute.KeyValue
|
||||
for _, a := range l.Attributes {
|
||||
kv, err := a.asAttributeKeyValue()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to decode link attribute (%s)", a.Key)
|
||||
}
|
||||
linkAttributes = append(linkAttributes, kv)
|
||||
}
|
||||
links = append(links, tracesdk.Link{
|
||||
SpanContext: linkSpanCtx,
|
||||
Attributes: linkAttributes,
|
||||
DroppedAttributeCount: l.DroppedAttributeCount,
|
||||
})
|
||||
}
|
||||
s.Links = links
|
||||
|
||||
var resources []attribute.KeyValue
|
||||
for _, r := range sd.Resource {
|
||||
kv, err := r.asAttributeKeyValue()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to decode resource (%s)", r.Key)
|
||||
}
|
||||
resources = append(resources, kv)
|
||||
}
|
||||
s.Resource = resources
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// asTraceSpanContext converts the internal spanContext representation to an
|
||||
// otel one.
|
||||
func (sc *spanContext) asTraceSpanContext() (trace.SpanContext, error) {
|
||||
traceID, err := traceIDFromHex(sc.TraceID)
|
||||
if err != nil {
|
||||
return trace.SpanContext{}, errors.Wrap(err, "unable to parse trace id")
|
||||
}
|
||||
spanID, err := spanIDFromHex(sc.SpanID)
|
||||
if err != nil {
|
||||
return trace.SpanContext{}, errors.Wrap(err, "unable to parse span id")
|
||||
}
|
||||
traceFlags := trace.TraceFlags(0x00)
|
||||
if sc.TraceFlags == "01" {
|
||||
traceFlags = trace.TraceFlags(0x01)
|
||||
}
|
||||
config := trace.SpanContextConfig{
|
||||
TraceID: traceID,
|
||||
SpanID: spanID,
|
||||
TraceFlags: traceFlags,
|
||||
Remote: sc.Remote,
|
||||
}
|
||||
return trace.NewSpanContext(config), nil
|
||||
}
|
||||
|
||||
// asAttributeKeyValue converts the internal keyValue representation to an
|
||||
// otel one.
|
||||
func (kv *keyValue) asAttributeKeyValue() (attribute.KeyValue, error) {
|
||||
// value types get encoded as string
|
||||
switch kv.Value.Type {
|
||||
case attribute.INVALID.String():
|
||||
return attribute.KeyValue{}, errors.New("invalid value type")
|
||||
case attribute.BOOL.String():
|
||||
return attribute.Bool(kv.Key, kv.Value.Value.(bool)), nil
|
||||
case attribute.INT64.String():
|
||||
// value could be int64 or float64, so handle both cases (float64 comes
|
||||
// from json unmarshal)
|
||||
var v int64
|
||||
switch i := kv.Value.Value.(type) {
|
||||
case int64:
|
||||
v = i
|
||||
case float64:
|
||||
v = int64(i)
|
||||
}
|
||||
return attribute.Int64(kv.Key, v), nil
|
||||
case attribute.FLOAT64.String():
|
||||
return attribute.Float64(kv.Key, kv.Value.Value.(float64)), nil
|
||||
case attribute.STRING.String():
|
||||
return attribute.String(kv.Key, kv.Value.Value.(string)), nil
|
||||
case attribute.BOOLSLICE.String():
|
||||
return attribute.BoolSlice(kv.Key, kv.Value.Value.([]bool)), nil
|
||||
case attribute.INT64SLICE.String():
|
||||
// handle both float64 and int64 (float64 comes from json unmarshal)
|
||||
var v []int64
|
||||
switch sli := kv.Value.Value.(type) {
|
||||
case []int64:
|
||||
v = sli
|
||||
case []float64:
|
||||
for i := range sli {
|
||||
v = append(v, int64(sli[i]))
|
||||
}
|
||||
}
|
||||
return attribute.Int64Slice(kv.Key, v), nil
|
||||
case attribute.FLOAT64SLICE.String():
|
||||
return attribute.Float64Slice(kv.Key, kv.Value.Value.([]float64)), nil
|
||||
case attribute.STRINGSLICE.String():
|
||||
var strSli []string
|
||||
// sometimes we can get an []interface{} instead of a []string, so
|
||||
// always cast to []string if that happens.
|
||||
switch sli := kv.Value.Value.(type) {
|
||||
case []string:
|
||||
strSli = sli
|
||||
case []interface{}:
|
||||
for i := range sli {
|
||||
var v string
|
||||
// best case we have a string, otherwise, cast it using
|
||||
// fmt.Sprintf
|
||||
if str, ok := sli[i].(string); ok {
|
||||
v = str
|
||||
} else {
|
||||
v = fmt.Sprintf("%v", sli[i])
|
||||
}
|
||||
// add the string to the slice
|
||||
strSli = append(strSli, v)
|
||||
}
|
||||
default:
|
||||
return attribute.KeyValue{}, errors.Errorf("got unsupported type %q for %s", reflect.ValueOf(kv.Value.Value).Kind(), attribute.STRINGSLICE.String())
|
||||
}
|
||||
return attribute.StringSlice(kv.Key, strSli), nil
|
||||
default:
|
||||
return attribute.KeyValue{}, errors.Errorf("unknown value type %s", kv.Value.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// traceIDFromHex returns a TraceID from a hex string if it is compliant with
|
||||
// the W3C trace-context specification and removes the validity check.
|
||||
// https://www.w3.org/TR/trace-context/#trace-id
|
||||
func traceIDFromHex(h string) (trace.TraceID, error) {
|
||||
t := trace.TraceID{}
|
||||
if len(h) != 32 {
|
||||
return t, errors.New("unable to parse trace id")
|
||||
}
|
||||
if err := decodeHex(h, t[:]); err != nil {
|
||||
return t, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// spanIDFromHex returns a SpanID from a hex string if it is compliant with the
|
||||
// W3C trace-context specification and removes the validity check.
|
||||
// https://www.w3.org/TR/trace-context/#parent-id
|
||||
func spanIDFromHex(h string) (trace.SpanID, error) {
|
||||
s := trace.SpanID{}
|
||||
if len(h) != 16 {
|
||||
return s, errors.New("unable to parse span id of length: %d")
|
||||
}
|
||||
if err := decodeHex(h, s[:]); err != nil {
|
||||
return s, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// decodeHex decodes hex in a manner compliant with otel.
|
||||
func decodeHex(h string, b []byte) error {
|
||||
for _, r := range h {
|
||||
switch {
|
||||
case 'a' <= r && r <= 'f':
|
||||
continue
|
||||
case '0' <= r && r <= '9':
|
||||
continue
|
||||
default:
|
||||
return errors.New("unable to parse hex id")
|
||||
}
|
||||
}
|
||||
decoded, err := hex.DecodeString(h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
copy(b, decoded)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Snapshot turns a Span into a ReadOnlySpan which is exportable by otel.
|
||||
func (s *Span) Snapshot() tracesdk.ReadOnlySpan {
|
||||
return spanSnapshot{
|
||||
name: s.Name,
|
||||
spanContext: s.SpanContext,
|
||||
parent: s.Parent,
|
||||
spanKind: s.SpanKind,
|
||||
startTime: s.StartTime,
|
||||
endTime: s.EndTime,
|
||||
attributes: s.Attributes,
|
||||
events: s.Events,
|
||||
links: s.Links,
|
||||
status: s.Status,
|
||||
droppedAttributes: s.DroppedAttributes,
|
||||
droppedEvents: s.DroppedEvents,
|
||||
droppedLinks: s.DroppedLinks,
|
||||
childSpanCount: s.ChildSpanCount,
|
||||
resource: resource.NewSchemaless(s.Resource...),
|
||||
instrumentationScope: s.InstrumentationLibrary,
|
||||
}
|
||||
}
|
||||
|
||||
// spanSnapshot is a helper type for transforming a Span into a ReadOnlySpan.
|
||||
type spanSnapshot struct {
|
||||
// Embed the interface to implement the private method.
|
||||
tracesdk.ReadOnlySpan
|
||||
|
||||
name string
|
||||
spanContext trace.SpanContext
|
||||
parent trace.SpanContext
|
||||
spanKind trace.SpanKind
|
||||
startTime time.Time
|
||||
endTime time.Time
|
||||
attributes []attribute.KeyValue
|
||||
events []tracesdk.Event
|
||||
links []tracesdk.Link
|
||||
status tracesdk.Status
|
||||
droppedAttributes int
|
||||
droppedEvents int
|
||||
droppedLinks int
|
||||
childSpanCount int
|
||||
resource *resource.Resource
|
||||
instrumentationScope instrumentation.Scope
|
||||
}
|
||||
|
||||
// Name returns the Name of the snapshot
|
||||
func (s spanSnapshot) Name() string { return s.name }
|
||||
|
||||
// SpanContext returns the SpanContext of the snapshot
|
||||
func (s spanSnapshot) SpanContext() trace.SpanContext { return s.spanContext }
|
||||
|
||||
// Parent returns the Parent of the snapshot
|
||||
func (s spanSnapshot) Parent() trace.SpanContext { return s.parent }
|
||||
|
||||
// SpanKind returns the SpanKind of the snapshot
|
||||
func (s spanSnapshot) SpanKind() trace.SpanKind { return s.spanKind }
|
||||
|
||||
// StartTime returns the StartTime of the snapshot
|
||||
func (s spanSnapshot) StartTime() time.Time { return s.startTime }
|
||||
|
||||
// EndTime returns the EndTime of the snapshot
|
||||
func (s spanSnapshot) EndTime() time.Time { return s.endTime }
|
||||
|
||||
// Attributes returns the Attributes of the snapshot
|
||||
func (s spanSnapshot) Attributes() []attribute.KeyValue { return s.attributes }
|
||||
|
||||
// Links returns the Links of the snapshot
|
||||
func (s spanSnapshot) Links() []tracesdk.Link { return s.links }
|
||||
|
||||
// Events return the Events of the snapshot
|
||||
func (s spanSnapshot) Events() []tracesdk.Event { return s.events }
|
||||
|
||||
// Status returns the Status of the snapshot
|
||||
func (s spanSnapshot) Status() tracesdk.Status { return s.status }
|
||||
|
||||
// DroppedAttributes returns the DroppedAttributes of the snapshot
|
||||
func (s spanSnapshot) DroppedAttributes() int { return s.droppedAttributes }
|
||||
|
||||
// DroppedLinks returns the DroppedLinks of the snapshot
|
||||
func (s spanSnapshot) DroppedLinks() int { return s.droppedLinks }
|
||||
|
||||
// DroppedEvents returns the DroppedEvents of the snapshot
|
||||
func (s spanSnapshot) DroppedEvents() int { return s.droppedEvents }
|
||||
|
||||
// ChildSpanCount returns the ChildSpanCount of the snapshot
|
||||
func (s spanSnapshot) ChildSpanCount() int { return s.childSpanCount }
|
||||
|
||||
// Resource returns the Resource of the snapshot
|
||||
func (s spanSnapshot) Resource() *resource.Resource { return s.resource }
|
||||
|
||||
// InstrumentationScope returns the InstrumentationScope of the snapshot
|
||||
func (s spanSnapshot) InstrumentationScope() instrumentation.Scope {
|
||||
return s.instrumentationScope
|
||||
}
|
||||
|
||||
// InstrumentationLibrary returns the InstrumentationLibrary of the snapshot
|
||||
//
|
||||
//nolint:staticcheck
|
||||
func (s spanSnapshot) InstrumentationLibrary() instrumentation.Library {
|
||||
return s.instrumentationScope
|
||||
}
|
159
util/otelutil/span_test.go
Normal file
159
util/otelutil/span_test.go
Normal file
@ -0,0 +1,159 @@
|
||||
package otelutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
|
||||
)
|
||||
|
||||
// curl -s --unix-socket /tmp/docker-desktop-build-dev.sock http://localhost/blobs/default/default?digest=sha256:3103104e9fa908087bd47572da6ad9a5a7bf973608f736536d18d635a7da0140 -X GET > ./fixtures/bktraces.json
|
||||
const bktracesFixture = "./fixtures/bktraces.json"
|
||||
|
||||
const otlpFixture = "./fixtures/otlp.json"
|
||||
|
||||
func TestParseSpanStubs(t *testing.T) {
|
||||
dt, err := os.ReadFile(bktracesFixture)
|
||||
require.NoError(t, err)
|
||||
|
||||
spanStubs, err := ParseSpanStubs(bytes.NewReader(dt))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 73, len(spanStubs))
|
||||
|
||||
dtSpanStubs, err := json.MarshalIndent(spanStubs, "", " ")
|
||||
require.NoError(t, err)
|
||||
dtotel, err := os.ReadFile(otlpFixture)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, string(dtotel), string(dtSpanStubs))
|
||||
|
||||
exp, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, exp.ExportSpans(context.Background(), spanStubs.ReadOnlySpans()))
|
||||
}
|
||||
|
||||
func TestAsAttributeKeyValue(t *testing.T) {
|
||||
type args struct {
|
||||
Type string
|
||||
value any
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want attribute.KeyValue
|
||||
}{
|
||||
{
|
||||
name: "string",
|
||||
args: args{
|
||||
Type: attribute.STRING.String(),
|
||||
value: "value",
|
||||
},
|
||||
want: attribute.String("key", "value"),
|
||||
},
|
||||
{
|
||||
name: "int64 (int64)",
|
||||
args: args{
|
||||
Type: attribute.INT64.String(),
|
||||
value: int64(1),
|
||||
},
|
||||
want: attribute.Int64("key", 1),
|
||||
},
|
||||
{
|
||||
name: "int64 (float64)",
|
||||
args: args{
|
||||
Type: attribute.INT64.String(),
|
||||
value: float64(1.0),
|
||||
},
|
||||
want: attribute.Int64("key", 1),
|
||||
},
|
||||
{
|
||||
name: "bool",
|
||||
args: args{
|
||||
Type: attribute.BOOL.String(),
|
||||
value: true,
|
||||
},
|
||||
want: attribute.Bool("key", true),
|
||||
},
|
||||
{
|
||||
name: "float64",
|
||||
args: args{
|
||||
Type: attribute.FLOAT64.String(),
|
||||
value: float64(1.0),
|
||||
},
|
||||
want: attribute.Float64("key", 1.0),
|
||||
},
|
||||
{
|
||||
name: "float64slice",
|
||||
args: args{
|
||||
Type: attribute.FLOAT64SLICE.String(),
|
||||
value: []float64{1.0, 2.0},
|
||||
},
|
||||
want: attribute.Float64Slice("key", []float64{1.0, 2.0}),
|
||||
},
|
||||
{
|
||||
name: "int64slice (int64)",
|
||||
args: args{
|
||||
Type: attribute.INT64SLICE.String(),
|
||||
value: []int64{1, 2},
|
||||
},
|
||||
want: attribute.Int64Slice("key", []int64{1, 2}),
|
||||
},
|
||||
{
|
||||
name: "int64slice (float64)",
|
||||
args: args{
|
||||
Type: attribute.INT64SLICE.String(),
|
||||
value: []float64{1.0, 2.0},
|
||||
},
|
||||
want: attribute.Int64Slice("key", []int64{1, 2}),
|
||||
},
|
||||
{
|
||||
name: "boolslice",
|
||||
args: args{
|
||||
Type: attribute.BOOLSLICE.String(),
|
||||
value: []bool{true, false},
|
||||
},
|
||||
want: attribute.BoolSlice("key", []bool{true, false}),
|
||||
},
|
||||
{
|
||||
name: "stringslice (strings)",
|
||||
args: args{
|
||||
Type: attribute.STRINGSLICE.String(),
|
||||
value: []string{"value1", "value2"},
|
||||
},
|
||||
want: attribute.StringSlice("key", []string{"value1", "value2"}),
|
||||
},
|
||||
{
|
||||
name: "stringslice (interface of string)",
|
||||
args: args{
|
||||
Type: attribute.STRINGSLICE.String(),
|
||||
value: []interface{}{"value1", "value2"},
|
||||
},
|
||||
want: attribute.StringSlice("key", []string{"value1", "value2"}),
|
||||
},
|
||||
{
|
||||
name: "stringslice (interface mixed)",
|
||||
args: args{
|
||||
Type: attribute.STRINGSLICE.String(),
|
||||
value: []interface{}{"value1", 2},
|
||||
},
|
||||
want: attribute.StringSlice("key", []string{"value1", "2"}),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
kv := keyValue{
|
||||
Key: "key",
|
||||
Value: value{Type: tt.args.Type, Value: tt.args.value},
|
||||
}
|
||||
attr, err := kv.asAttributeKeyValue()
|
||||
require.NoError(t, err, "failed to convert key value to attribute key value")
|
||||
assert.Equal(t, tt.want, attr, "attribute key value mismatch")
|
||||
})
|
||||
}
|
||||
}
|
34
vendor/github.com/tonistiigi/jaeger-ui-rest/Dockerfile
generated
vendored
Normal file
34
vendor/github.com/tonistiigi/jaeger-ui-rest/Dockerfile
generated
vendored
Normal 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
42
vendor/github.com/tonistiigi/jaeger-ui-rest/config.go
generated
vendored
Normal 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
|
||||
}
|
117
vendor/github.com/tonistiigi/jaeger-ui-rest/decompress/decompress.go
generated
vendored
Normal file
117
vendor/github.com/tonistiigi/jaeger-ui-rest/decompress/decompress.go
generated
vendored
Normal 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{}
|
3
vendor/github.com/tonistiigi/jaeger-ui-rest/docker-bake.hcl
generated
vendored
Normal file
3
vendor/github.com/tonistiigi/jaeger-ui-rest/docker-bake.hcl
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
target "public" {
|
||||
output = ["./public"]
|
||||
}
|
17
vendor/github.com/tonistiigi/jaeger-ui-rest/fs.go
generated
vendored
Normal file
17
vendor/github.com/tonistiigi/jaeger-ui-rest/fs.go
generated
vendored
Normal 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))
|
||||
}
|
BIN
vendor/github.com/tonistiigi/jaeger-ui-rest/public/index.html
generated
vendored
Normal file
BIN
vendor/github.com/tonistiigi/jaeger-ui-rest/public/index.html
generated
vendored
Normal file
Binary file not shown.
BIN
vendor/github.com/tonistiigi/jaeger-ui-rest/public/static/favicon-BxcVf0am.ico
generated
vendored
Normal file
BIN
vendor/github.com/tonistiigi/jaeger-ui-rest/public/static/favicon-BxcVf0am.ico
generated
vendored
Normal file
Binary file not shown.
BIN
vendor/github.com/tonistiigi/jaeger-ui-rest/public/static/index-BBZlPGVK.js
generated
vendored
Normal file
BIN
vendor/github.com/tonistiigi/jaeger-ui-rest/public/static/index-BBZlPGVK.js
generated
vendored
Normal file
Binary file not shown.
BIN
vendor/github.com/tonistiigi/jaeger-ui-rest/public/static/index-C4KU8NTf.css
generated
vendored
Normal file
BIN
vendor/github.com/tonistiigi/jaeger-ui-rest/public/static/index-C4KU8NTf.css
generated
vendored
Normal file
Binary file not shown.
BIN
vendor/github.com/tonistiigi/jaeger-ui-rest/public/static/jaeger-logo-CNZsoUdk.svg
generated
vendored
Normal file
BIN
vendor/github.com/tonistiigi/jaeger-ui-rest/public/static/jaeger-logo-CNZsoUdk.svg
generated
vendored
Normal file
Binary file not shown.
3
vendor/github.com/tonistiigi/jaeger-ui-rest/readme.md
generated
vendored
Normal file
3
vendor/github.com/tonistiigi/jaeger-ui-rest/readme.md
generated
vendored
Normal 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
172
vendor/github.com/tonistiigi/jaeger-ui-rest/server.go
generated
vendored
Normal 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)
|
||||
}
|
||||
}
|
201
vendor/go.opentelemetry.io/otel/exporters/stdout/stdouttrace/LICENSE
generated
vendored
Normal file
201
vendor/go.opentelemetry.io/otel/exporters/stdout/stdouttrace/LICENSE
generated
vendored
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
3
vendor/go.opentelemetry.io/otel/exporters/stdout/stdouttrace/README.md
generated
vendored
Normal file
3
vendor/go.opentelemetry.io/otel/exporters/stdout/stdouttrace/README.md
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# STDOUT Trace Exporter
|
||||
|
||||
[](https://pkg.go.dev/go.opentelemetry.io/otel/exporters/stdout/stdouttrace)
|
85
vendor/go.opentelemetry.io/otel/exporters/stdout/stdouttrace/config.go
generated
vendored
Normal file
85
vendor/go.opentelemetry.io/otel/exporters/stdout/stdouttrace/config.go
generated
vendored
Normal file
@ -0,0 +1,85 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package stdouttrace // import "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultWriter = os.Stdout
|
||||
defaultPrettyPrint = false
|
||||
defaultTimestamps = true
|
||||
)
|
||||
|
||||
// config contains options for the STDOUT exporter.
|
||||
type config struct {
|
||||
// Writer is the destination. If not set, os.Stdout is used.
|
||||
Writer io.Writer
|
||||
|
||||
// PrettyPrint will encode the output into readable JSON. Default is
|
||||
// false.
|
||||
PrettyPrint bool
|
||||
|
||||
// Timestamps specifies if timestamps should be printed. Default is
|
||||
// true.
|
||||
Timestamps bool
|
||||
}
|
||||
|
||||
// newConfig creates a validated Config configured with options.
|
||||
func newConfig(options ...Option) config {
|
||||
cfg := config{
|
||||
Writer: defaultWriter,
|
||||
PrettyPrint: defaultPrettyPrint,
|
||||
Timestamps: defaultTimestamps,
|
||||
}
|
||||
for _, opt := range options {
|
||||
cfg = opt.apply(cfg)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// Option sets the value of an option for a Config.
|
||||
type Option interface {
|
||||
apply(config) config
|
||||
}
|
||||
|
||||
// WithWriter sets the export stream destination.
|
||||
func WithWriter(w io.Writer) Option {
|
||||
return writerOption{w}
|
||||
}
|
||||
|
||||
type writerOption struct {
|
||||
W io.Writer
|
||||
}
|
||||
|
||||
func (o writerOption) apply(cfg config) config {
|
||||
cfg.Writer = o.W
|
||||
return cfg
|
||||
}
|
||||
|
||||
// WithPrettyPrint prettifies the emitted output.
|
||||
func WithPrettyPrint() Option {
|
||||
return prettyPrintOption(true)
|
||||
}
|
||||
|
||||
type prettyPrintOption bool
|
||||
|
||||
func (o prettyPrintOption) apply(cfg config) config {
|
||||
cfg.PrettyPrint = bool(o)
|
||||
return cfg
|
||||
}
|
||||
|
||||
// WithoutTimestamps sets the export stream to not include timestamps.
|
||||
func WithoutTimestamps() Option {
|
||||
return timestampsOption(false)
|
||||
}
|
||||
|
||||
type timestampsOption bool
|
||||
|
||||
func (o timestampsOption) apply(cfg config) config {
|
||||
cfg.Timestamps = bool(o)
|
||||
return cfg
|
||||
}
|
6
vendor/go.opentelemetry.io/otel/exporters/stdout/stdouttrace/doc.go
generated
vendored
Normal file
6
vendor/go.opentelemetry.io/otel/exporters/stdout/stdouttrace/doc.go
generated
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package stdouttrace contains an OpenTelemetry exporter for tracing
|
||||
// telemetry to be written to an output destination as JSON.
|
||||
package stdouttrace // import "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
|
103
vendor/go.opentelemetry.io/otel/exporters/stdout/stdouttrace/trace.go
generated
vendored
Normal file
103
vendor/go.opentelemetry.io/otel/exporters/stdout/stdouttrace/trace.go
generated
vendored
Normal file
@ -0,0 +1,103 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package stdouttrace // import "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/sdk/trace"
|
||||
"go.opentelemetry.io/otel/sdk/trace/tracetest"
|
||||
)
|
||||
|
||||
var zeroTime time.Time
|
||||
|
||||
var _ trace.SpanExporter = &Exporter{}
|
||||
|
||||
// New creates an Exporter with the passed options.
|
||||
func New(options ...Option) (*Exporter, error) {
|
||||
cfg := newConfig(options...)
|
||||
|
||||
enc := json.NewEncoder(cfg.Writer)
|
||||
if cfg.PrettyPrint {
|
||||
enc.SetIndent("", "\t")
|
||||
}
|
||||
|
||||
return &Exporter{
|
||||
encoder: enc,
|
||||
timestamps: cfg.Timestamps,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Exporter is an implementation of trace.SpanSyncer that writes spans to stdout.
|
||||
type Exporter struct {
|
||||
encoder *json.Encoder
|
||||
encoderMu sync.Mutex
|
||||
timestamps bool
|
||||
|
||||
stoppedMu sync.RWMutex
|
||||
stopped bool
|
||||
}
|
||||
|
||||
// ExportSpans writes spans in json format to stdout.
|
||||
func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
e.stoppedMu.RLock()
|
||||
stopped := e.stopped
|
||||
e.stoppedMu.RUnlock()
|
||||
if stopped {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(spans) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
stubs := tracetest.SpanStubsFromReadOnlySpans(spans)
|
||||
|
||||
e.encoderMu.Lock()
|
||||
defer e.encoderMu.Unlock()
|
||||
for i := range stubs {
|
||||
stub := &stubs[i]
|
||||
// Remove timestamps
|
||||
if !e.timestamps {
|
||||
stub.StartTime = zeroTime
|
||||
stub.EndTime = zeroTime
|
||||
for j := range stub.Events {
|
||||
ev := &stub.Events[j]
|
||||
ev.Time = zeroTime
|
||||
}
|
||||
}
|
||||
|
||||
// Encode span stubs, one by one
|
||||
if err := e.encoder.Encode(stub); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown is called to stop the exporter, it performs no action.
|
||||
func (e *Exporter) Shutdown(ctx context.Context) error {
|
||||
e.stoppedMu.Lock()
|
||||
e.stopped = true
|
||||
e.stoppedMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalLog is the marshaling function used by the logging system to represent this Exporter.
|
||||
func (e *Exporter) MarshalLog() interface{} {
|
||||
return struct {
|
||||
Type string
|
||||
WithTimestamps bool
|
||||
}{
|
||||
Type: "stdout",
|
||||
WithTimestamps: e.timestamps,
|
||||
}
|
||||
}
|
7
vendor/modules.txt
vendored
7
vendor/modules.txt
vendored
@ -737,6 +737,10 @@ github.com/tonistiigi/fsutil/types
|
||||
# github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4
|
||||
## explicit; go 1.16
|
||||
github.com/tonistiigi/go-csvvalue
|
||||
# github.com/tonistiigi/jaeger-ui-rest v0.0.0-20250211190051-7d4944a45bb6
|
||||
## explicit; go 1.22.0
|
||||
github.com/tonistiigi/jaeger-ui-rest
|
||||
github.com/tonistiigi/jaeger-ui-rest/decompress
|
||||
# github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea
|
||||
## explicit
|
||||
github.com/tonistiigi/units
|
||||
@ -828,6 +832,9 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/envconfig
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/otlpconfig
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/retry
|
||||
# go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0
|
||||
## explicit; go 1.22
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace
|
||||
# go.opentelemetry.io/otel/metric v1.31.0
|
||||
## explicit; go 1.22
|
||||
go.opentelemetry.io/otel/metric
|
||||
|
Loading…
x
Reference in New Issue
Block a user