mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-05-20 18:26:29 +03:00
Compare commits
7 Commits
weakpointe
...
victoriatr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a890a81ad9 | ||
|
|
4429ac3664 | ||
|
|
9364c1ee93 | ||
|
|
16975f413f | ||
|
|
35d009fdd1 | ||
|
|
f2ba60e3ee | ||
|
|
960b1e8428 |
@@ -110,4 +110,4 @@ run-victoria-logs:
|
||||
DOCKER_OPTS='-v $(shell pwd)/victoria-logs-data:/victoria-logs-data' \
|
||||
APP_NAME=victoria-logs \
|
||||
ARGS='' \
|
||||
$(MAKE) run-via-docker
|
||||
$(MAKE) run-via-docker
|
||||
113
app/victoria-traces/Makefile
Normal file
113
app/victoria-traces/Makefile
Normal file
@@ -0,0 +1,113 @@
|
||||
# All these commands must run from repository root.
|
||||
|
||||
victoria-traces:
|
||||
APP_NAME=victoria-traces $(MAKE) app-local
|
||||
|
||||
victoria-traces-race:
|
||||
APP_NAME=victoria-traces RACE=-race $(MAKE) app-local
|
||||
|
||||
victoria-traces-prod:
|
||||
APP_NAME=victoria-traces $(MAKE) app-via-docker
|
||||
|
||||
victoria-traces-pure-prod:
|
||||
APP_NAME=victoria-traces $(MAKE) app-via-docker-pure
|
||||
|
||||
victoria-traces-linux-amd64-prod:
|
||||
APP_NAME=victoria-traces $(MAKE) app-via-docker-linux-amd64
|
||||
|
||||
victoria-traces-linux-arm-prod:
|
||||
APP_NAME=victoria-traces $(MAKE) app-via-docker-linux-arm
|
||||
|
||||
victoria-traces-linux-arm64-prod:
|
||||
APP_NAME=victoria-traces $(MAKE) app-via-docker-linux-arm64
|
||||
|
||||
victoria-traces-linux-ppc64le-prod:
|
||||
APP_NAME=victoria-traces $(MAKE) app-via-docker-linux-ppc64le
|
||||
|
||||
victoria-traces-linux-386-prod:
|
||||
APP_NAME=victoria-traces $(MAKE) app-via-docker-linux-386
|
||||
|
||||
victoria-traces-darwin-amd64-prod:
|
||||
APP_NAME=victoria-traces $(MAKE) app-via-docker-darwin-amd64
|
||||
|
||||
victoria-traces-darwin-arm64-prod:
|
||||
APP_NAME=victoria-traces $(MAKE) app-via-docker-darwin-arm64
|
||||
|
||||
victoria-traces-freebsd-amd64-prod:
|
||||
APP_NAME=victoria-traces $(MAKE) app-via-docker-freebsd-amd64
|
||||
|
||||
victoria-traces-openbsd-amd64-prod:
|
||||
APP_NAME=victoria-traces $(MAKE) app-via-docker-openbsd-amd64
|
||||
|
||||
victoria-traces-windows-amd64-prod:
|
||||
APP_NAME=victoria-traces $(MAKE) app-via-docker-windows-amd64
|
||||
|
||||
package-victoria-traces:
|
||||
APP_NAME=victoria-traces $(MAKE) package-via-docker
|
||||
|
||||
package-victoria-traces-pure:
|
||||
APP_NAME=victoria-traces $(MAKE) package-via-docker-pure
|
||||
|
||||
package-victoria-traces-amd64:
|
||||
APP_NAME=victoria-traces $(MAKE) package-via-docker-amd64
|
||||
|
||||
package-victoria-traces-arm:
|
||||
APP_NAME=victoria-traces $(MAKE) package-via-docker-arm
|
||||
|
||||
package-victoria-traces-arm64:
|
||||
APP_NAME=victoria-traces $(MAKE) package-via-docker-arm64
|
||||
|
||||
package-victoria-traces-ppc64le:
|
||||
APP_NAME=victoria-traces $(MAKE) package-via-docker-ppc64le
|
||||
|
||||
package-victoria-traces-386:
|
||||
APP_NAME=victoria-traces $(MAKE) package-via-docker-386
|
||||
|
||||
publish-victoria-traces:
|
||||
APP_NAME=victoria-traces $(MAKE) publish-via-docker
|
||||
|
||||
victoria-traces-linux-amd64:
|
||||
APP_NAME=victoria-traces CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
victoria-traces-linux-arm:
|
||||
APP_NAME=victoria-traces CGO_ENABLED=0 GOOS=linux GOARCH=arm $(MAKE) app-local-goos-goarch
|
||||
|
||||
victoria-traces-linux-arm64:
|
||||
APP_NAME=victoria-traces CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
victoria-traces-linux-ppc64le:
|
||||
APP_NAME=victoria-traces CGO_ENABLED=0 GOOS=linux GOARCH=ppc64le $(MAKE) app-local-goos-goarch
|
||||
|
||||
victoria-traces-linux-s390x:
|
||||
APP_NAME=victoria-traces CGO_ENABLED=0 GOOS=linux GOARCH=s390x $(MAKE) app-local-goos-goarch
|
||||
|
||||
victoria-traces-linux-loong64:
|
||||
APP_NAME=victoria-traces CGO_ENABLED=0 GOOS=linux GOARCH=loong64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
victoria-traces-linux-386:
|
||||
APP_NAME=victoria-traces CGO_ENABLED=0 GOOS=linux GOARCH=386 $(MAKE) app-local-goos-goarch
|
||||
|
||||
victoria-traces-darwin-amd64:
|
||||
APP_NAME=victoria-traces CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
victoria-traces-darwin-arm64:
|
||||
APP_NAME=victoria-traces CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
victoria-traces-freebsd-amd64:
|
||||
APP_NAME=victoria-traces CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
victoria-traces-openbsd-amd64:
|
||||
APP_NAME=victoria-traces CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
victoria-traces-windows-amd64:
|
||||
GOARCH=amd64 APP_NAME=victoria-traces $(MAKE) app-local-windows-goarch
|
||||
|
||||
victoria-traces-pure:
|
||||
APP_NAME=victoria-traces $(MAKE) app-local-pure
|
||||
|
||||
run-victoria-traces:
|
||||
mkdir -p victoria-traces-data
|
||||
DOCKER_OPTS='-v $(shell pwd)/victoria-traces-data:/victoria-traces-data' \
|
||||
APP_NAME=victoria-traces \
|
||||
ARGS='' \
|
||||
$(MAKE) run-via-docker
|
||||
8
app/victoria-traces/deployment/Dockerfile
Normal file
8
app/victoria-traces/deployment/Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
ARG base_image=non-existing
|
||||
FROM $base_image
|
||||
|
||||
EXPOSE 9428
|
||||
|
||||
ENTRYPOINT ["/victoria-traces-prod"]
|
||||
ARG src_binary=non-existing
|
||||
COPY $src_binary ./victoria-traces-prod
|
||||
120
app/victoria-traces/main.go
Normal file
120
app/victoria-traces/main.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlselect"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlstorage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envflag"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/pushmetrics"
|
||||
)
|
||||
|
||||
var (
|
||||
httpListenAddrs = flagutil.NewArrayString("httpListenAddr", "TCP address to listen for incoming http requests. See also -httpListenAddr.useProxyProtocol")
|
||||
useProxyProtocol = flagutil.NewArrayBool("httpListenAddr.useProxyProtocol", "Whether to use proxy protocol for connections accepted at the given -httpListenAddr . "+
|
||||
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt . "+
|
||||
"With enabled proxy protocol http server cannot serve regular /metrics endpoint. Use -pushmetrics.url for metrics pushing")
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Write flags and help message to stdout, since it is easier to grep or pipe.
|
||||
flag.CommandLine.SetOutput(os.Stdout)
|
||||
flag.Usage = usage
|
||||
envflag.Parse()
|
||||
|
||||
// update default storage path to ./victoria-traces-data
|
||||
storagePath := flag.Lookup("storageDataPath")
|
||||
if storagePath != nil && storagePath.Value.String() == "victoria-logs-data" {
|
||||
storagePath.Value.Set("victoria-traces-data")
|
||||
}
|
||||
|
||||
buildinfo.Init()
|
||||
logger.Init()
|
||||
|
||||
listenAddrs := *httpListenAddrs
|
||||
if len(listenAddrs) == 0 {
|
||||
listenAddrs = []string{":9428"}
|
||||
}
|
||||
logger.Infof("starting VictoriaLogs at %q...", listenAddrs)
|
||||
startTime := time.Now()
|
||||
|
||||
vlstorage.Init()
|
||||
vlselect.Init()
|
||||
|
||||
insertutil.SetLogRowsStorage(&vlstorage.Storage{})
|
||||
vlinsert.Init()
|
||||
|
||||
go httpserver.Serve(listenAddrs, requestHandler, httpserver.ServeOptions{
|
||||
UseProxyProtocol: useProxyProtocol,
|
||||
})
|
||||
logger.Infof("started VictoriaLogs in %.3f seconds; see https://docs.victoriametrics.com/victorialogs/", time.Since(startTime).Seconds())
|
||||
|
||||
pushmetrics.Init()
|
||||
sig := procutil.WaitForSigterm()
|
||||
logger.Infof("received signal %s", sig)
|
||||
pushmetrics.Stop()
|
||||
|
||||
logger.Infof("gracefully shutting down webservice at %q", listenAddrs)
|
||||
startTime = time.Now()
|
||||
if err := httpserver.Stop(listenAddrs); err != nil {
|
||||
logger.Fatalf("cannot stop the webservice: %s", err)
|
||||
}
|
||||
logger.Infof("successfully shut down the webservice in %.3f seconds", time.Since(startTime).Seconds())
|
||||
|
||||
vlinsert.Stop()
|
||||
vlselect.Stop()
|
||||
vlstorage.Stop()
|
||||
|
||||
fs.MustStopDirRemover()
|
||||
|
||||
logger.Infof("the VictoriaLogs has been stopped in %.3f seconds", time.Since(startTime).Seconds())
|
||||
}
|
||||
|
||||
func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
if r.URL.Path == "/" {
|
||||
if r.Method != http.MethodGet {
|
||||
return false
|
||||
}
|
||||
w.Header().Add("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprintf(w, "<h2>Single-node VictoriaLogs</h2></br>")
|
||||
fmt.Fprintf(w, "See docs at <a href='https://docs.victoriametrics.com/victorialogs/'>https://docs.victoriametrics.com/victorialogs/</a></br>")
|
||||
fmt.Fprintf(w, "Useful endpoints:</br>")
|
||||
httpserver.WriteAPIHelp(w, [][2]string{
|
||||
{"select/vmui", "Web UI for VictoriaLogs"},
|
||||
{"metrics", "available service metrics"},
|
||||
{"flags", "command-line flags"},
|
||||
})
|
||||
return true
|
||||
}
|
||||
if vlinsert.RequestHandler(w, r) {
|
||||
return true
|
||||
}
|
||||
if vlselect.RequestHandler(w, r) {
|
||||
return true
|
||||
}
|
||||
if vlstorage.RequestHandler(w, r) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func usage() {
|
||||
const s = `
|
||||
victoria-logs is a log management and analytics service.
|
||||
|
||||
See the docs at https://docs.victoriametrics.com/victorialogs/
|
||||
`
|
||||
flagutil.Usage(s)
|
||||
}
|
||||
12
app/victoria-traces/multiarch/Dockerfile
Normal file
12
app/victoria-traces/multiarch/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
# See https://medium.com/on-docker/use-multi-stage-builds-to-inject-ca-certs-ad1e8f01de1b
|
||||
ARG certs_image=non-existing
|
||||
ARG root_image=non-existing
|
||||
FROM $certs_image AS certs
|
||||
RUN apk update && apk upgrade && apk --update --no-cache add ca-certificates
|
||||
|
||||
FROM $root_image
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
EXPOSE 9428
|
||||
ENTRYPOINT ["/victoria-traces-prod"]
|
||||
ARG TARGETARCH
|
||||
COPY victoria-traces-linux-${TARGETARCH}-prod ./victoria-traces-prod
|
||||
@@ -3,12 +3,13 @@ package elasticsearch
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/snappy"
|
||||
"github.com/klauspost/compress/gzip"
|
||||
"github.com/klauspost/compress/zlib"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutil"
|
||||
)
|
||||
|
||||
137
app/vlinsert/opentelemetry/logs/logs.go
Normal file
137
app/vlinsert/opentelemetry/logs/logs.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package logs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/pb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
var maxRequestSize = flagutil.NewBytes("opentelemetry.maxRequestSize", 64*1024*1024, "The maximum size in bytes of a single OpenTelemetry request")
|
||||
|
||||
// HandleProtobuf handles the log ingestion request.
|
||||
func HandleProtobuf(r *http.Request, w http.ResponseWriter) {
|
||||
startTime := time.Now()
|
||||
requestsProtobufTotal.Inc()
|
||||
|
||||
cp, err := insertutil.GetCommonParams(r)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "cannot parse common params from request: %s", err)
|
||||
return
|
||||
}
|
||||
if err := insertutil.CanWriteData(); err != nil {
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
encoding := r.Header.Get("Content-Encoding")
|
||||
err = protoparserutil.ReadUncompressedData(r.Body, encoding, maxRequestSize, func(data []byte) error {
|
||||
lmp := cp.NewLogMessageProcessor("opentelelemtry_protobuf", false)
|
||||
useDefaultStreamFields := len(cp.StreamFields) == 0
|
||||
err := pushProtobufRequest(data, lmp, cp.MsgFields, useDefaultStreamFields)
|
||||
lmp.MustClose()
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "cannot read OpenTelemetry protocol data: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// update requestProtobufDuration only for successfully parsed requests
|
||||
// There is no need in updating requestProtobufDuration for request errors,
|
||||
// since their timings are usually much smaller than the timing for successful request parsing.
|
||||
requestProtobufDuration.UpdateDuration(startTime)
|
||||
}
|
||||
|
||||
var (
|
||||
requestsProtobufTotal = metrics.NewCounter(`vl_http_requests_total{path="/insert/opentelemetry/v1/logs",format="protobuf"}`)
|
||||
errorsTotal = metrics.NewCounter(`vl_http_errors_total{path="/insert/opentelemetry/v1/logs",format="protobuf"}`)
|
||||
|
||||
requestProtobufDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/insert/opentelemetry/v1/logs",format="protobuf"}`)
|
||||
)
|
||||
|
||||
func pushProtobufRequest(data []byte, lmp insertutil.LogMessageProcessor, msgFields []string, useDefaultStreamFields bool) error {
|
||||
var req pb.ExportLogsServiceRequest
|
||||
if err := req.UnmarshalProtobuf(data); err != nil {
|
||||
errorsTotal.Inc()
|
||||
return fmt.Errorf("cannot unmarshal request from %d bytes: %w", len(data), err)
|
||||
}
|
||||
|
||||
var commonFields []logstorage.Field
|
||||
for _, rl := range req.ResourceLogs {
|
||||
commonFields = commonFields[:0]
|
||||
commonFields = appendKeyValues(commonFields, rl.Resource.Attributes, "")
|
||||
commonFieldsLen := len(commonFields)
|
||||
for _, sc := range rl.ScopeLogs {
|
||||
commonFields = pushFieldsFromScopeLogs(&sc, commonFields[:commonFieldsLen], lmp, msgFields, useDefaultStreamFields)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pushFieldsFromScopeLogs(sc *pb.ScopeLogs, commonFields []logstorage.Field, lmp insertutil.LogMessageProcessor, msgFields []string, useDefaultStreamFields bool) []logstorage.Field {
|
||||
fields := commonFields
|
||||
for _, lr := range sc.LogRecords {
|
||||
fields = fields[:len(commonFields)]
|
||||
if lr.Body.KeyValueList != nil {
|
||||
fields = appendKeyValues(fields, lr.Body.KeyValueList.Values, "")
|
||||
logstorage.RenameField(fields[len(commonFields):], msgFields, "_msg")
|
||||
} else {
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: "_msg",
|
||||
Value: lr.Body.FormatString(true),
|
||||
})
|
||||
}
|
||||
fields = appendKeyValues(fields, lr.Attributes, "")
|
||||
if len(lr.TraceID) > 0 {
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: "trace_id",
|
||||
Value: lr.TraceID,
|
||||
})
|
||||
}
|
||||
if len(lr.SpanID) > 0 {
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: "span_id",
|
||||
Value: lr.SpanID,
|
||||
})
|
||||
}
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: "severity",
|
||||
Value: lr.FormatSeverity(),
|
||||
})
|
||||
|
||||
var streamFields []logstorage.Field
|
||||
if useDefaultStreamFields {
|
||||
streamFields = commonFields
|
||||
}
|
||||
lmp.AddRow(lr.ExtractTimestampNano(), fields, streamFields)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func appendKeyValues(fields []logstorage.Field, kvs []*pb.KeyValue, parentField string) []logstorage.Field {
|
||||
for _, attr := range kvs {
|
||||
fieldName := attr.Key
|
||||
if parentField != "" {
|
||||
fieldName = parentField + "." + fieldName
|
||||
}
|
||||
|
||||
if attr.Value.KeyValueList != nil {
|
||||
fields = appendKeyValues(fields, attr.Value.KeyValueList.Values, fieldName)
|
||||
} else {
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: fieldName,
|
||||
Value: attr.Value.FormatString(true),
|
||||
})
|
||||
}
|
||||
}
|
||||
return fields
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package opentelemetry
|
||||
package logs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
package opentelemetry
|
||||
package logs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,21 +1,13 @@
|
||||
package opentelemetry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/opentelemetry/logs"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/opentelemetry/traces"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/pb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
var maxRequestSize = flagutil.NewBytes("opentelemetry.maxRequestSize", 64*1024*1024, "The maximum size in bytes of a single OpenTelemetry request")
|
||||
|
||||
// RequestHandler processes Opentelemetry insert requests
|
||||
func RequestHandler(path string, w http.ResponseWriter, r *http.Request) bool {
|
||||
switch path {
|
||||
@@ -26,128 +18,18 @@ func RequestHandler(path string, w http.ResponseWriter, r *http.Request) bool {
|
||||
httpserver.Errorf(w, r, "json encoding isn't supported for opentelemetry format. Use protobuf encoding")
|
||||
return true
|
||||
}
|
||||
handleProtobuf(r, w)
|
||||
logs.HandleProtobuf(r, w)
|
||||
return true
|
||||
// use the same path as opentelemetry collector
|
||||
// https://opentelemetry.io/docs/specs/otlp/#otlphttp-request
|
||||
case "/insert/opentelemetry/v1/traces":
|
||||
if r.Header.Get("Content-Type") == "application/json" {
|
||||
httpserver.Errorf(w, r, "json encoding isn't supported for opentelemetry format. Use protobuf encoding")
|
||||
return true
|
||||
}
|
||||
traces.HandleProtobuf(r, w)
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func handleProtobuf(r *http.Request, w http.ResponseWriter) {
|
||||
startTime := time.Now()
|
||||
requestsProtobufTotal.Inc()
|
||||
|
||||
cp, err := insertutil.GetCommonParams(r)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "cannot parse common params from request: %s", err)
|
||||
return
|
||||
}
|
||||
if err := insertutil.CanWriteData(); err != nil {
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
encoding := r.Header.Get("Content-Encoding")
|
||||
err = protoparserutil.ReadUncompressedData(r.Body, encoding, maxRequestSize, func(data []byte) error {
|
||||
lmp := cp.NewLogMessageProcessor("opentelelemtry_protobuf", false)
|
||||
useDefaultStreamFields := len(cp.StreamFields) == 0
|
||||
err := pushProtobufRequest(data, lmp, cp.MsgFields, useDefaultStreamFields)
|
||||
lmp.MustClose()
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "cannot read OpenTelemetry protocol data: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// update requestProtobufDuration only for successfully parsed requests
|
||||
// There is no need in updating requestProtobufDuration for request errors,
|
||||
// since their timings are usually much smaller than the timing for successful request parsing.
|
||||
requestProtobufDuration.UpdateDuration(startTime)
|
||||
}
|
||||
|
||||
var (
|
||||
requestsProtobufTotal = metrics.NewCounter(`vl_http_requests_total{path="/insert/opentelemetry/v1/logs",format="protobuf"}`)
|
||||
errorsTotal = metrics.NewCounter(`vl_http_errors_total{path="/insert/opentelemetry/v1/logs",format="protobuf"}`)
|
||||
|
||||
requestProtobufDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/insert/opentelemetry/v1/logs",format="protobuf"}`)
|
||||
)
|
||||
|
||||
func pushProtobufRequest(data []byte, lmp insertutil.LogMessageProcessor, msgFields []string, useDefaultStreamFields bool) error {
|
||||
var req pb.ExportLogsServiceRequest
|
||||
if err := req.UnmarshalProtobuf(data); err != nil {
|
||||
errorsTotal.Inc()
|
||||
return fmt.Errorf("cannot unmarshal request from %d bytes: %w", len(data), err)
|
||||
}
|
||||
|
||||
var commonFields []logstorage.Field
|
||||
for _, rl := range req.ResourceLogs {
|
||||
commonFields = commonFields[:0]
|
||||
commonFields = appendKeyValues(commonFields, rl.Resource.Attributes, "")
|
||||
commonFieldsLen := len(commonFields)
|
||||
for _, sc := range rl.ScopeLogs {
|
||||
commonFields = pushFieldsFromScopeLogs(&sc, commonFields[:commonFieldsLen], lmp, msgFields, useDefaultStreamFields)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pushFieldsFromScopeLogs(sc *pb.ScopeLogs, commonFields []logstorage.Field, lmp insertutil.LogMessageProcessor, msgFields []string, useDefaultStreamFields bool) []logstorage.Field {
|
||||
fields := commonFields
|
||||
for _, lr := range sc.LogRecords {
|
||||
fields = fields[:len(commonFields)]
|
||||
if lr.Body.KeyValueList != nil {
|
||||
fields = appendKeyValues(fields, lr.Body.KeyValueList.Values, "")
|
||||
logstorage.RenameField(fields[len(commonFields):], msgFields, "_msg")
|
||||
} else {
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: "_msg",
|
||||
Value: lr.Body.FormatString(true),
|
||||
})
|
||||
}
|
||||
fields = appendKeyValues(fields, lr.Attributes, "")
|
||||
if len(lr.TraceID) > 0 {
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: "trace_id",
|
||||
Value: lr.TraceID,
|
||||
})
|
||||
}
|
||||
if len(lr.SpanID) > 0 {
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: "span_id",
|
||||
Value: lr.SpanID,
|
||||
})
|
||||
}
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: "severity",
|
||||
Value: lr.FormatSeverity(),
|
||||
})
|
||||
|
||||
var streamFields []logstorage.Field
|
||||
if useDefaultStreamFields {
|
||||
streamFields = commonFields
|
||||
}
|
||||
lmp.AddRow(lr.ExtractTimestampNano(), fields, streamFields)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func appendKeyValues(fields []logstorage.Field, kvs []*pb.KeyValue, parentField string) []logstorage.Field {
|
||||
for _, attr := range kvs {
|
||||
fieldName := attr.Key
|
||||
if parentField != "" {
|
||||
fieldName = parentField + "." + fieldName
|
||||
}
|
||||
|
||||
if attr.Value.KeyValueList != nil {
|
||||
fields = appendKeyValues(fields, attr.Value.KeyValueList.Values, fieldName)
|
||||
} else {
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: fieldName,
|
||||
Value: attr.Value.FormatString(true),
|
||||
})
|
||||
}
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
187
app/vlinsert/opentelemetry/traces/traces.go
Normal file
187
app/vlinsert/opentelemetry/traces/traces.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package traces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
|
||||
otelpb "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/pb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
var maxRequestSize = flagutil.NewBytes("opentelemetry.traces.maxRequestSize", 64*1024*1024, "The maximum size in bytes of a single OpenTelemetry trace export request.")
|
||||
|
||||
var (
|
||||
requestsProtobufTotal = metrics.NewCounter(`vl_http_requests_total{path="/insert/opentelemetry/v1/traces",format="protobuf"}`)
|
||||
errorsTotal = metrics.NewCounter(`vl_http_errors_total{path="/insert/opentelemetry/v1/traces",format="protobuf"}`)
|
||||
|
||||
requestProtobufDuration = metrics.NewHistogram(`vl_http_request_duration_seconds{path="/insert/opentelemetry/v1/traces",format="protobuf"}`)
|
||||
)
|
||||
|
||||
var (
|
||||
mandatoryStreamFields = []string{otelpb.ResourceAttrServiceName, otelpb.NameField}
|
||||
msgFieldValue = "-"
|
||||
)
|
||||
|
||||
// HandleProtobuf handles the trace ingestion request.
|
||||
func HandleProtobuf(r *http.Request, w http.ResponseWriter) {
|
||||
startTime := time.Now()
|
||||
requestsProtobufTotal.Inc()
|
||||
|
||||
cp, err := insertutil.GetCommonParams(r)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "cannot parse common params from request: %s", err)
|
||||
return
|
||||
}
|
||||
// stream fields must contain the service name and span name.
|
||||
// by using arguments and headers, users can also add other fields as stream fields
|
||||
// for potentially better efficiency.
|
||||
cp.StreamFields = append(mandatoryStreamFields, cp.StreamFields...)
|
||||
|
||||
if err := insertutil.CanWriteData(); err != nil {
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
encoding := r.Header.Get("Content-Encoding")
|
||||
err = protoparserutil.ReadUncompressedData(r.Body, encoding, maxRequestSize, func(data []byte) error {
|
||||
lmp := cp.NewLogMessageProcessor("opentelemetry_traces_protobuf", false)
|
||||
err := pushProtobufRequest(data, lmp)
|
||||
lmp.MustClose()
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "cannot read OpenTelemetry protocol data: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// update requestProtobufDuration only for successfully parsed requests
|
||||
// There is no need in updating requestProtobufDuration for request errors,
|
||||
// since their timings are usually much smaller than the timing for successful request parsing.
|
||||
requestProtobufDuration.UpdateDuration(startTime)
|
||||
}
|
||||
|
||||
func pushProtobufRequest(data []byte, lmp insertutil.LogMessageProcessor) error {
|
||||
var req otelpb.ExportTraceServiceRequest
|
||||
if err := req.UnmarshalProtobuf(data); err != nil {
|
||||
errorsTotal.Inc()
|
||||
return fmt.Errorf("cannot unmarshal request from %d bytes: %w", len(data), err)
|
||||
}
|
||||
|
||||
var commonFields []logstorage.Field
|
||||
for _, rs := range req.ResourceSpans {
|
||||
commonFields = commonFields[:0]
|
||||
attributes := rs.Resource.Attributes
|
||||
commonFields = appendKeyValuesWithPrefix(commonFields, attributes, "", otelpb.ResourceAttrPrefix)
|
||||
commonFieldsLen := len(commonFields)
|
||||
for _, ss := range rs.ScopeSpans {
|
||||
commonFields = pushFieldsFromScopeSpans(ss, commonFields[:commonFieldsLen], lmp)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pushFieldsFromScopeSpans(ss *otelpb.ScopeSpans, commonFields []logstorage.Field, lmp insertutil.LogMessageProcessor) []logstorage.Field {
|
||||
commonFields = append(commonFields, logstorage.Field{
|
||||
Name: otelpb.InstrumentationScopeName,
|
||||
Value: ss.Scope.Name,
|
||||
}, logstorage.Field{
|
||||
Name: otelpb.InstrumentationScopeVersion,
|
||||
Value: ss.Scope.Version,
|
||||
})
|
||||
commonFields = appendKeyValuesWithPrefix(commonFields, ss.Scope.Attributes, "", otelpb.InstrumentationScopeAttrPrefix)
|
||||
commonFieldsLen := len(commonFields)
|
||||
for _, span := range ss.Spans {
|
||||
commonFields = pushFieldsFromSpan(span, commonFields[:commonFieldsLen], lmp)
|
||||
}
|
||||
return commonFields
|
||||
}
|
||||
|
||||
func pushFieldsFromSpan(span *otelpb.Span, scopeCommonFields []logstorage.Field, lmp insertutil.LogMessageProcessor) []logstorage.Field {
|
||||
fields := scopeCommonFields
|
||||
fields = append(fields,
|
||||
logstorage.Field{Name: otelpb.TraceIDField, Value: span.TraceID},
|
||||
logstorage.Field{Name: otelpb.SpanIDField, Value: span.SpanID},
|
||||
logstorage.Field{Name: otelpb.TraceStateField, Value: span.TraceState},
|
||||
logstorage.Field{Name: otelpb.ParentSpanIDField, Value: span.ParentSpanID},
|
||||
logstorage.Field{Name: otelpb.FlagsField, Value: strconv.FormatUint(uint64(span.Flags), 10)},
|
||||
logstorage.Field{Name: otelpb.NameField, Value: span.Name},
|
||||
logstorage.Field{Name: otelpb.KindField, Value: strconv.FormatInt(int64(span.Kind), 10)},
|
||||
logstorage.Field{Name: otelpb.StartTimeUnixNanoField, Value: strconv.FormatUint(span.StartTimeUnixNano, 10)},
|
||||
logstorage.Field{Name: otelpb.EndTimeUnixNanoField, Value: strconv.FormatUint(span.EndTimeUnixNano, 10)},
|
||||
logstorage.Field{Name: otelpb.DurationField, Value: strconv.FormatUint(span.EndTimeUnixNano-span.StartTimeUnixNano, 10)},
|
||||
|
||||
logstorage.Field{Name: otelpb.DroppedAttributesCountField, Value: strconv.FormatUint(uint64(span.DroppedAttributesCount), 10)},
|
||||
logstorage.Field{Name: otelpb.DroppedEventsCountField, Value: strconv.FormatUint(uint64(span.DroppedEventsCount), 10)},
|
||||
logstorage.Field{Name: otelpb.DroppedLinksCountField, Value: strconv.FormatUint(uint64(span.DroppedLinksCount), 10)},
|
||||
|
||||
logstorage.Field{Name: otelpb.StatusMessageField, Value: span.Status.Message},
|
||||
logstorage.Field{Name: otelpb.StatusCodeField, Value: strconv.FormatInt(int64(span.Status.Code), 10)},
|
||||
)
|
||||
|
||||
// append span attributes
|
||||
fields = appendKeyValuesWithPrefix(fields, span.Attributes, "", otelpb.SpanAttrPrefixField)
|
||||
|
||||
for idx, event := range span.Events {
|
||||
eventFieldPrefix := otelpb.EventPrefix + strconv.Itoa(idx) + ":"
|
||||
fields = append(fields,
|
||||
logstorage.Field{Name: eventFieldPrefix + otelpb.EventTimeUnixNanoField, Value: strconv.FormatUint(event.TimeUnixNano, 10)},
|
||||
logstorage.Field{Name: eventFieldPrefix + otelpb.EventNameField, Value: event.Name},
|
||||
logstorage.Field{Name: eventFieldPrefix + otelpb.EventDroppedAttributesCountField, Value: strconv.FormatUint(uint64(event.DroppedAttributesCount), 10)},
|
||||
)
|
||||
// append event attributes
|
||||
fields = appendKeyValuesWithPrefix(fields, event.Attributes, "", eventFieldPrefix+otelpb.EventAttrPrefix)
|
||||
}
|
||||
|
||||
for idx, link := range span.Links {
|
||||
linkFieldPrefix := otelpb.LinkPrefix + strconv.Itoa(idx) + ":"
|
||||
|
||||
fields = append(fields,
|
||||
logstorage.Field{Name: linkFieldPrefix + otelpb.LinkTraceIDField, Value: link.TraceID},
|
||||
logstorage.Field{Name: linkFieldPrefix + otelpb.LinkSpanIDField, Value: link.SpanID},
|
||||
logstorage.Field{Name: linkFieldPrefix + otelpb.LinkTraceStateField, Value: link.TraceState},
|
||||
logstorage.Field{Name: linkFieldPrefix + otelpb.LinkDroppedAttributesCountField, Value: strconv.FormatUint(uint64(link.DroppedAttributesCount), 10)},
|
||||
logstorage.Field{Name: linkFieldPrefix + otelpb.LinkFlagsField, Value: strconv.FormatUint(uint64(link.Flags), 10)},
|
||||
)
|
||||
|
||||
// append link attributes
|
||||
fields = appendKeyValuesWithPrefix(fields, link.Attributes, "", linkFieldPrefix+otelpb.LinkAttrPrefix)
|
||||
}
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: "_msg",
|
||||
Value: msgFieldValue,
|
||||
})
|
||||
lmp.AddRow(int64(span.EndTimeUnixNano), fields, nil)
|
||||
return fields
|
||||
}
|
||||
|
||||
func appendKeyValuesWithPrefix(fields []logstorage.Field, kvs []*otelpb.KeyValue, parentField, prefix string) []logstorage.Field {
|
||||
for _, attr := range kvs {
|
||||
fieldName := attr.Key
|
||||
if parentField != "" {
|
||||
fieldName = parentField + "." + fieldName
|
||||
}
|
||||
|
||||
if attr.Value.KeyValueList != nil {
|
||||
fields = appendKeyValuesWithPrefix(fields, attr.Value.KeyValueList.Values, fieldName, prefix)
|
||||
continue
|
||||
}
|
||||
|
||||
v := attr.Value.FormatString(true)
|
||||
if len(v) == 0 {
|
||||
// VictoriaLogs does not support empty string as field value. set it to "-" to preserve the field.
|
||||
v = "-"
|
||||
}
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: prefix + fieldName,
|
||||
Value: v,
|
||||
})
|
||||
}
|
||||
return fields
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlselect/internalselect"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlselect/logsql"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlselect/traces/jaeger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
|
||||
@@ -140,6 +141,19 @@ func selectHandler(w http.ResponseWriter, r *http.Request, path string) bool {
|
||||
}
|
||||
defer decRequestConcurrency()
|
||||
|
||||
if strings.HasPrefix(path, "/internal/select/") {
|
||||
// Process internal request from vlselect without timeout (e.g. use ctx instead of ctxWithTimeout),
|
||||
// since the timeout must be controlled by the vlselect.
|
||||
internalselect.RequestHandler(ctx, w, r)
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(path, "/select/jaeger/") {
|
||||
// Jaeger HTTP APIs for distributed tracing.
|
||||
// Could be used by Grafana Jaeger datasource, Jaeger UI, and more.
|
||||
return jaeger.RequestHandler(ctxWithTimeout, w, r)
|
||||
}
|
||||
|
||||
ok := processSelectRequest(ctxWithTimeout, w, r, path)
|
||||
if !ok {
|
||||
return false
|
||||
|
||||
401
app/vlselect/traces/jaeger/jaeger.go
Normal file
401
app/vlselect/traces/jaeger/jaeger.go
Normal file
@@ -0,0 +1,401 @@
|
||||
package jaeger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlselect/traces/query"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/hashpool"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
otelpb "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/pb"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
const (
|
||||
maxLimit = 1000
|
||||
)
|
||||
|
||||
// Jaeger Query APIs metrics
|
||||
var (
|
||||
jaegerServicesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/jaeger/api/services"}`)
|
||||
jaegerServicesDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/jaeger/api/services"}`)
|
||||
|
||||
jaegerOperationsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/jaeger/api/services/*/operations"}`)
|
||||
jaegerOperationsDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/jaeger/api/services/*/operations"}`)
|
||||
|
||||
jaegerTracesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/jaeger/api/traces"}`)
|
||||
jaegerTracesDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/jaeger/api/traces"}`)
|
||||
|
||||
jaegerTraceRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/jaeger/api/traces/*"}`)
|
||||
jaegerTraceDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/jaeger/api/traces/*"}`)
|
||||
|
||||
jaegerDependenciesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/jaeger/api/dependencies"}`)
|
||||
jaegerDependenciesDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/jaeger/api/dependencies"}`)
|
||||
)
|
||||
|
||||
// RequestHandler is the entry point for all Jaeger query APIs.
|
||||
// Jaeger JSON API is intentionally undocumented.
|
||||
// So APIs are based on the implementation in Jaeger repository.
|
||||
// See:
|
||||
// 1. https://www.jaegertracing.io/docs/1.70/architecture/apis/#http-json-internal
|
||||
// 2. https://github.com/jaegertracing/jaeger/blob/9a45f522422c548827b2f3897affc8170e4a3d8b/cmd/query/app/http_handler.go#L110
|
||||
func RequestHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) bool {
|
||||
httpserver.EnableCORS(w, r)
|
||||
startTime := time.Now()
|
||||
path := r.URL.Path
|
||||
if path == "/select/jaeger/api/services" {
|
||||
jaegerServicesRequests.Inc()
|
||||
processGetServicesRequest(ctx, w, r)
|
||||
jaegerServicesDuration.UpdateDuration(startTime)
|
||||
return true
|
||||
} else if strings.HasPrefix(path, "/select/jaeger/api/services/") && strings.HasSuffix(path, "/operations") {
|
||||
jaegerOperationsRequests.Inc()
|
||||
processGetOperationsRequest(ctx, w, r)
|
||||
jaegerOperationsDuration.UpdateDuration(startTime)
|
||||
return true
|
||||
} else if path == "/select/jaeger/api/traces" {
|
||||
jaegerTracesRequests.Inc()
|
||||
processGetTracesRequest(ctx, w, r)
|
||||
jaegerTracesDuration.UpdateDuration(startTime)
|
||||
return true
|
||||
} else if strings.HasPrefix(path, "/select/jaeger/api/traces/") && len(path) > len("/select/jaeger/api/traces/") {
|
||||
jaegerTraceRequests.Inc()
|
||||
processGetTraceRequest(ctx, w, r)
|
||||
jaegerTraceDuration.UpdateDuration(startTime)
|
||||
return true
|
||||
} else if path == "/select/jaeger/api/dependencies" {
|
||||
jaegerDependenciesRequests.Inc()
|
||||
// todo it require additional component to calculate the dependency graph. not implemented yet.
|
||||
httpserver.Errorf(w, r, "/api/dependencies API is not supported yet.")
|
||||
jaegerDependenciesDuration.UpdateDuration(startTime)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// processGetServicesRequest handle the Jaeger /api/services API request.
|
||||
// https://github.com/jaegertracing/jaeger/blob/9a45f522422c548827b2f3897affc8170e4a3d8b/cmd/query/app/http_handler.go#L146
|
||||
func processGetServicesRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
cp, err := query.GetCommonParams(r)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "incorrect query params: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
serviceList, err := query.GetServiceNameList(ctx, cp)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "cannot get services list: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Write results
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
WriteGetServicesResponse(w, serviceList)
|
||||
}
|
||||
|
||||
// processGetOperationsRequest handle the Jaeger /api/services/<service_name>/operations API request.
|
||||
// https://github.com/jaegertracing/jaeger/blob/9a45f522422c548827b2f3897affc8170e4a3d8b/cmd/query/app/http_handler.go#L158
|
||||
func processGetOperationsRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
cp, err := query.GetCommonParams(r)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "incorrect query params: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// extract the `service_name`.
|
||||
// the path must be like `/select/jaeger/api/services/<service_name>/operations`.
|
||||
u := r.URL.Path[len("/select/jaeger/api/services/"):]
|
||||
|
||||
// check for invalid path: /select/jaeger/api/services/operations
|
||||
if !strings.Contains(u, "/") {
|
||||
httpserver.Errorf(w, r, "incorrect query path [%s]", r.URL.Path)
|
||||
return
|
||||
}
|
||||
serviceName := u[:len(u)-len("/operations")]
|
||||
if len(serviceName) == 0 {
|
||||
httpserver.Errorf(w, r, "incorrect query path [%s]", r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
operationList, err := query.GetSpanNameList(ctx, cp, serviceName)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "cannot get operation list: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Write results
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
WriteGetOperationsResponse(w, operationList)
|
||||
}
|
||||
|
||||
// processGetTraceRequest handle the Jaeger /api/traces/<trace_id> API request.
|
||||
// https://github.com/jaegertracing/jaeger/blob/9a45f522422c548827b2f3897affc8170e4a3d8b/cmd/query/app/http_handler.go#L465
|
||||
func processGetTraceRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
cp, err := query.GetCommonParams(r)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "incorrect query params: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// extract the `trace_id`.
|
||||
// the path must be like `/select/jaeger/api/traces/<trace_id>`.
|
||||
traceID := r.URL.Path[len("/select/jaeger/api/traces/"):]
|
||||
if len(traceID) == 0 {
|
||||
httpserver.Errorf(w, r, "incorrect query path [%s]", r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := query.GetTrace(ctx, cp, traceID)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "cannot get traces: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
t := &trace{}
|
||||
processHashIDMap := make(map[uint64]string) // process name -> process id
|
||||
processIDProcessMap := make(map[string]process) // process id -> process
|
||||
for i := range rows {
|
||||
var sp *span
|
||||
sp, err = fieldsToSpan(rows[i].Fields)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot unmarshal log fields [%v] to span: %s", rows[i].Fields, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Process ID
|
||||
processHash := hashProcess(sp.process)
|
||||
if _, ok := processHashIDMap[processHash]; !ok {
|
||||
processID := "p" + strconv.Itoa(len(processHashIDMap)+1)
|
||||
processHashIDMap[processHash] = processID
|
||||
processIDProcessMap[processID] = sp.process
|
||||
}
|
||||
|
||||
sp.processID = processHashIDMap[processHash]
|
||||
t.spans = append(t.spans, sp)
|
||||
}
|
||||
|
||||
// 6. attach process info to this trace
|
||||
t.processMap = make([]processMap, 0, len(processIDProcessMap))
|
||||
for processID, p := range processIDProcessMap {
|
||||
t.processMap = append(t.processMap, processMap{
|
||||
processID: processID,
|
||||
process: p,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(t.processMap, func(i, j int) bool {
|
||||
return t.processMap[i].processID < t.processMap[j].processID
|
||||
})
|
||||
|
||||
// Write results
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
WriteGetTracesResponse(w, []*trace{t})
|
||||
}
|
||||
|
||||
// processGetTracesRequest handle the Jaeger /api/traces API request.
|
||||
// https://github.com/jaegertracing/jaeger/blob/9a45f522422c548827b2f3897affc8170e4a3d8b/cmd/query/app/http_handler.go#L227
|
||||
func processGetTracesRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
cp, err := query.GetCommonParams(r)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "incorrect query params: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
param, err := parseJaegerTraceQueryParam(ctx, r)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "incorrect trace query params: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
traceIDList, rows, err := query.GetTraceList(ctx, cp, param)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "get trace list error: %s", err)
|
||||
return
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
// Write empty results
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
WriteGetTracesResponse(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// convert fields spans to jaeger spans, and group by trace_id.
|
||||
//
|
||||
// 1. prepare a trace_id -> *trace map
|
||||
tracesMap := make(map[string]*trace)
|
||||
traces := make([]*trace, len(traceIDList))
|
||||
for i := range traceIDList {
|
||||
traces[i] = &trace{}
|
||||
tracesMap[traceIDList[i]] = traces[i]
|
||||
}
|
||||
|
||||
processHashMap := make(map[uint64]string) // process_unique_hash -> pid
|
||||
traceProcessMap := make(map[string]map[string]process) // trace_id -> map[processID]->process
|
||||
for i := range rows {
|
||||
// 2. convert fields to jaeger spans.
|
||||
var sp *span
|
||||
sp, err = fieldsToSpan(rows[i].Fields)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot unmarshal log fields [%v] to span: %s", rows[i].Fields, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 3. calculate the process that this span belongs to
|
||||
procHash := hashProcess(sp.process)
|
||||
if _, ok := processHashMap[procHash]; !ok {
|
||||
// format process id as Jaeger does: `p{idx}`, where {idx} starts from 1.
|
||||
processHashMap[procHash] = "p" + strconv.Itoa(len(processHashMap)+1)
|
||||
}
|
||||
// and attach the process info to the span.
|
||||
sp.processID = processHashMap[procHash]
|
||||
|
||||
// 4. add the process info to this trace (if process not exists).
|
||||
if _, ok := traceProcessMap[sp.traceID]; !ok {
|
||||
traceProcessMap[sp.traceID] = make(map[string]process)
|
||||
}
|
||||
if _, ok := traceProcessMap[sp.traceID][sp.processID]; !ok {
|
||||
traceProcessMap[sp.traceID][sp.processID] = sp.process
|
||||
}
|
||||
|
||||
// 5. append this span to the trace it belongs to.
|
||||
tracesMap[sp.traceID].spans = append(tracesMap[sp.traceID].spans, sp)
|
||||
}
|
||||
|
||||
// 6. attach process info to each trace
|
||||
for traceID, trace := range tracesMap {
|
||||
trace.processMap = make([]processMap, 0, len(traceProcessMap[traceID]))
|
||||
for processID, process := range traceProcessMap[traceID] {
|
||||
trace.processMap = append(trace.processMap, processMap{
|
||||
processID: processID,
|
||||
process: process,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(trace.processMap, func(i, j int) bool {
|
||||
return trace.processMap[i].processID < trace.processMap[j].processID
|
||||
})
|
||||
}
|
||||
|
||||
// Write results
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
WriteGetTracesResponse(w, traces)
|
||||
}
|
||||
|
||||
// parseJaegerTraceQueryParam parse Jaeger request to unified query.TraceQueryParam.
|
||||
func parseJaegerTraceQueryParam(_ context.Context, r *http.Request) (*query.TraceQueryParam, error) {
|
||||
var err error
|
||||
|
||||
// default params
|
||||
p := &query.TraceQueryParam{
|
||||
StartTimeMin: time.Unix(0, 0),
|
||||
StartTimeMax: time.Now(),
|
||||
Limit: 20,
|
||||
}
|
||||
q := r.URL.Query()
|
||||
if p.ServiceName = q.Get("service"); p.ServiceName == "" {
|
||||
// service name is required.
|
||||
return nil, fmt.Errorf("service name is required")
|
||||
}
|
||||
p.SpanName = q.Get("operation")
|
||||
durationMin := q.Get("minDuration")
|
||||
if durationMin != "" {
|
||||
p.DurationMin, err = time.ParseDuration(durationMin)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse minDuration [%s]: %w", durationMin, err)
|
||||
}
|
||||
}
|
||||
|
||||
durationMax := q.Get("maxDuration")
|
||||
if durationMax != "" {
|
||||
p.DurationMax, err = time.ParseDuration(durationMax)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse maxDuration [%s]: %w", durationMax, err)
|
||||
}
|
||||
}
|
||||
|
||||
limit := q.Get("limit")
|
||||
if limit != "" {
|
||||
p.Limit, err = strconv.Atoi(limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse limit [%s]: %w", limit, err)
|
||||
}
|
||||
if p.Limit > maxLimit {
|
||||
return nil, fmt.Errorf("limit should be not higher than %d", maxLimit)
|
||||
}
|
||||
}
|
||||
|
||||
startTimeMin := q.Get("start")
|
||||
if startTimeMin != "" {
|
||||
unixNano, err := strconv.ParseInt(startTimeMin, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse start [%s]: %w", startTimeMin, err)
|
||||
}
|
||||
p.StartTimeMin = time.UnixMicro(unixNano)
|
||||
}
|
||||
|
||||
startTimeMax := q.Get("end")
|
||||
if startTimeMax != "" {
|
||||
unixNano, err := strconv.ParseInt(startTimeMax, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse end [%s]: %w", startTimeMax, err)
|
||||
}
|
||||
p.StartTimeMax = time.UnixMicro(unixNano)
|
||||
}
|
||||
|
||||
tags := q.Get("tags")
|
||||
if tags != "" {
|
||||
if err := json.Unmarshal([]byte(tags), &p.Attributes); err != nil {
|
||||
return nil, fmt.Errorf("cannot parse tags [%s]: %w", tags, err)
|
||||
}
|
||||
}
|
||||
|
||||
attributesFilter := make(map[string]string, len(p.Attributes))
|
||||
// some special fields in the OpenTelemetry span will be treated as span attributes/tags
|
||||
// in query result, so they should be converted to proper filters correspondingly.
|
||||
// e.g.: `otel.status_description` attribute in query result could be:
|
||||
// 1. retrieved from `span_attr:otel.status_description` field directly.
|
||||
// 2. converted from `status_message` field for Jaeger API.
|
||||
for k, v := range p.Attributes {
|
||||
// convert to OpenTelemetry field name in storage.
|
||||
if field, ok := spanAttributeMap[k]; ok {
|
||||
// 2 special cases that need to converted value as well.
|
||||
if k == "error" {
|
||||
v = errorStatusCodeMap[v]
|
||||
} else if k == "span.kind" {
|
||||
v = spanKindMap[v]
|
||||
}
|
||||
attributesFilter[field] = v
|
||||
} else if strings.HasPrefix(k, otelpb.InstrumentationScopeAttrPrefix) || strings.HasPrefix(k, otelpb.ResourceAttrPrefix) {
|
||||
attributesFilter[k] = v
|
||||
} else {
|
||||
attributesFilter[otelpb.SpanAttrPrefixField+k] = v
|
||||
}
|
||||
}
|
||||
p.Attributes = attributesFilter
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// hashProcess generate hash result for a process according to its tags.
|
||||
func hashProcess(process process) uint64 {
|
||||
d := hashpool.Get()
|
||||
sort.Slice(process.tags, func(i, j int) bool {
|
||||
return process.tags[i].key < process.tags[j].key
|
||||
})
|
||||
_, _ = d.WriteString(process.serviceName)
|
||||
for _, tag := range process.tags {
|
||||
_, _ = d.WriteString(tag.key)
|
||||
_, _ = d.WriteString(tag.vStr)
|
||||
}
|
||||
h := d.Sum64()
|
||||
d.Reset()
|
||||
hashpool.Put(d)
|
||||
return h
|
||||
}
|
||||
169
app/vlselect/traces/jaeger/jaeger.qtpl
Normal file
169
app/vlselect/traces/jaeger/jaeger.qtpl
Normal file
@@ -0,0 +1,169 @@
|
||||
{% import (
|
||||
"sort"
|
||||
) %}
|
||||
|
||||
{% stripspace %}
|
||||
|
||||
{% func GetServicesResponse(serviceList []string) %}
|
||||
{
|
||||
{% code
|
||||
sort.Slice(serviceList, func(i, j int) bool { return serviceList[i] < serviceList[j] })
|
||||
%}
|
||||
"data":[
|
||||
{% if len(serviceList) > 0 %}
|
||||
{%q= serviceList[0] %}
|
||||
{% for _, service := range serviceList[1:] %}
|
||||
,{%q= service %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
],
|
||||
"errors": null,
|
||||
"limit": 0,
|
||||
"offset": 0,
|
||||
"total": {%d= len(serviceList) %}
|
||||
}
|
||||
{% endfunc %}
|
||||
|
||||
{% func GetOperationsResponse(operationList []string) %}
|
||||
{
|
||||
{% code
|
||||
sort.Slice(operationList, func(i, j int) bool { return operationList[i] < operationList[j] })
|
||||
%}
|
||||
"data":[
|
||||
{% if len(operationList) > 0 %}
|
||||
{%q= operationList[0] %}
|
||||
{% for _, operation := range operationList[1:] %}
|
||||
,{%q= operation %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
],
|
||||
"errors": null,
|
||||
"limit": 0,
|
||||
"offset": 0,
|
||||
"total": {%d= len(operationList) %}
|
||||
}
|
||||
{% endfunc %}
|
||||
|
||||
{% func GetTracesResponse(traces []*trace) %}
|
||||
{
|
||||
"data":[
|
||||
{% if len(traces) > 0 && len(traces[0].spans) > 0 %}
|
||||
{%= traceJson(traces[0]) %}
|
||||
{% for _, trace := range traces[1:] %}
|
||||
{% if len(trace.spans) > 0 %}
|
||||
,{%= traceJson(trace) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
],
|
||||
"errors": null,
|
||||
"limit": 0,
|
||||
"offset": 0,
|
||||
"total": {%d= len(traces) %}
|
||||
}
|
||||
{% endfunc %}
|
||||
|
||||
{% func traceJson(trace *trace) %}
|
||||
{
|
||||
"processes": {
|
||||
{% if len(trace.processMap) > 0 %}
|
||||
{%q= trace.processMap[0].processID %}:{%= processJson(trace.processMap[0].process) %}
|
||||
{% for _, v := range trace.processMap[1:] %}
|
||||
,{%q= v.processID %}:{%= processJson(v.process) %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
},
|
||||
"spans": [
|
||||
{% if len(trace.spans) > 0 %}
|
||||
{%= spanJson(trace.spans[0]) %}
|
||||
{% for _, v := range trace.spans[1:] %}
|
||||
,{%= spanJson(v) %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
],
|
||||
"traceID": {%q= trace.spans[0].traceID %},
|
||||
"warnings": null
|
||||
}
|
||||
{% endfunc %}
|
||||
|
||||
{% func processJson(process process) %}
|
||||
{
|
||||
"serviceName": {%q= process.serviceName %},
|
||||
"tags": [
|
||||
{% if len(process.tags) > 0 %}
|
||||
{%= tagJson(process.tags[0]) %}
|
||||
{% for _, v := range process.tags[1:] %}
|
||||
,{%= tagJson(v) %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
]
|
||||
}
|
||||
{% endfunc %}
|
||||
|
||||
{% func spanJson(span *span) %}
|
||||
{
|
||||
"duration":{%dl= span.duration %},
|
||||
"logs":[
|
||||
{% if len(span.logs) > 0 %}
|
||||
{%= logJson(span.logs[0]) %}
|
||||
{% for _, v := range span.logs[1:] %}
|
||||
,{%= logJson(v) %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
],
|
||||
"operationName":{%q= span.operationName %},
|
||||
"processID":{%q= span.processID %},
|
||||
"references": [
|
||||
{% if len(span.references) > 0 %}
|
||||
{%= spanRefJson(span.references[0]) %}
|
||||
{% for _, v := range span.references[1:] %}
|
||||
,{%= spanRefJson(v) %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
],
|
||||
"spanID":{%q= span.spanID %},
|
||||
"startTime":{%dl= span.startTime %},
|
||||
"tags": [
|
||||
{% if len(span.tags) > 0 %}
|
||||
{%= tagJson(span.tags[0]) %}
|
||||
{% for _, v := range span.tags[1:] %}
|
||||
,{%= tagJson(v) %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
],
|
||||
"traceID":{%q= span.traceID %},
|
||||
"warnings":null
|
||||
}
|
||||
{% endfunc %}
|
||||
|
||||
{% func tagJson(tag keyValue) %}
|
||||
{
|
||||
"key":{%q= tag.key %},
|
||||
"type":"string",
|
||||
"value":{%q= tag.vStr %}
|
||||
}
|
||||
{% endfunc %}
|
||||
|
||||
{% func logJson(l log) %}
|
||||
{
|
||||
"timestamp":{%dl= l.timestamp %},
|
||||
"fields":[
|
||||
{% if len(l.fields) > 0 %}
|
||||
{%= tagJson(l.fields[0]) %}
|
||||
{% for _, v := range l.fields[1:] %}
|
||||
,{%= tagJson(v) %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
]
|
||||
}
|
||||
{% endfunc %}
|
||||
|
||||
{% func spanRefJson(ref spanRef) %}
|
||||
{
|
||||
"refType":{%q= ref.refType %},
|
||||
"spanID":{%q= ref.spanID %},
|
||||
"traceID":{%q= ref.traceID %}
|
||||
}
|
||||
{% endfunc %}
|
||||
|
||||
{% endstripspace %}
|
||||
570
app/vlselect/traces/jaeger/jaeger.qtpl.go
Normal file
570
app/vlselect/traces/jaeger/jaeger.qtpl.go
Normal file
@@ -0,0 +1,570 @@
|
||||
// Code generated by qtc from "jaeger.qtpl". DO NOT EDIT.
|
||||
// See https://github.com/valyala/quicktemplate for details.
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:1
|
||||
package jaeger
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:1
|
||||
import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:7
|
||||
import (
|
||||
qtio422016 "io"
|
||||
|
||||
qt422016 "github.com/valyala/quicktemplate"
|
||||
)
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:7
|
||||
var (
|
||||
_ = qtio422016.Copy
|
||||
_ = qt422016.AcquireByteBuffer
|
||||
)
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:7
|
||||
func StreamGetServicesResponse(qw422016 *qt422016.Writer, serviceList []string) {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:7
|
||||
qw422016.N().S(`{`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:10
|
||||
sort.Slice(serviceList, func(i, j int) bool { return serviceList[i] < serviceList[j] })
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:11
|
||||
qw422016.N().S(`"data":[`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:13
|
||||
if len(serviceList) > 0 {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:14
|
||||
qw422016.N().Q(serviceList[0])
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:15
|
||||
for _, service := range serviceList[1:] {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:15
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:16
|
||||
qw422016.N().Q(service)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:17
|
||||
}
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:18
|
||||
}
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:18
|
||||
qw422016.N().S(`],"errors": null,"limit": 0,"offset": 0,"total":`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:23
|
||||
qw422016.N().D(len(serviceList))
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:23
|
||||
qw422016.N().S(`}`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:25
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:25
|
||||
func WriteGetServicesResponse(qq422016 qtio422016.Writer, serviceList []string) {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:25
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:25
|
||||
StreamGetServicesResponse(qw422016, serviceList)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:25
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:25
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:25
|
||||
func GetServicesResponse(serviceList []string) string {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:25
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:25
|
||||
WriteGetServicesResponse(qb422016, serviceList)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:25
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:25
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:25
|
||||
return qs422016
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:25
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:27
|
||||
func StreamGetOperationsResponse(qw422016 *qt422016.Writer, operationList []string) {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:27
|
||||
qw422016.N().S(`{`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:30
|
||||
sort.Slice(operationList, func(i, j int) bool { return operationList[i] < operationList[j] })
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:31
|
||||
qw422016.N().S(`"data":[`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:33
|
||||
if len(operationList) > 0 {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:34
|
||||
qw422016.N().Q(operationList[0])
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:35
|
||||
for _, operation := range operationList[1:] {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:35
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:36
|
||||
qw422016.N().Q(operation)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:37
|
||||
}
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:38
|
||||
}
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:38
|
||||
qw422016.N().S(`],"errors": null,"limit": 0,"offset": 0,"total":`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:43
|
||||
qw422016.N().D(len(operationList))
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:43
|
||||
qw422016.N().S(`}`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:45
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:45
|
||||
func WriteGetOperationsResponse(qq422016 qtio422016.Writer, operationList []string) {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:45
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:45
|
||||
StreamGetOperationsResponse(qw422016, operationList)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:45
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:45
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:45
|
||||
func GetOperationsResponse(operationList []string) string {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:45
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:45
|
||||
WriteGetOperationsResponse(qb422016, operationList)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:45
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:45
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:45
|
||||
return qs422016
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:45
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:47
|
||||
func StreamGetTracesResponse(qw422016 *qt422016.Writer, traces []*trace) {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:47
|
||||
qw422016.N().S(`{"data":[`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:50
|
||||
if len(traces) > 0 && len(traces[0].spans) > 0 {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:51
|
||||
streamtraceJson(qw422016, traces[0])
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:52
|
||||
for _, trace := range traces[1:] {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:53
|
||||
if len(trace.spans) > 0 {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:53
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:54
|
||||
streamtraceJson(qw422016, trace)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:55
|
||||
}
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:56
|
||||
}
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:57
|
||||
}
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:57
|
||||
qw422016.N().S(`],"errors": null,"limit": 0,"offset": 0,"total":`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:62
|
||||
qw422016.N().D(len(traces))
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:62
|
||||
qw422016.N().S(`}`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:64
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:64
|
||||
func WriteGetTracesResponse(qq422016 qtio422016.Writer, traces []*trace) {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:64
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:64
|
||||
StreamGetTracesResponse(qw422016, traces)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:64
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:64
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:64
|
||||
func GetTracesResponse(traces []*trace) string {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:64
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:64
|
||||
WriteGetTracesResponse(qb422016, traces)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:64
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:64
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:64
|
||||
return qs422016
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:64
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:66
|
||||
func streamtraceJson(qw422016 *qt422016.Writer, trace *trace) {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:66
|
||||
qw422016.N().S(`{"processes": {`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:69
|
||||
if len(trace.processMap) > 0 {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:70
|
||||
qw422016.N().Q(trace.processMap[0].processID)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:70
|
||||
qw422016.N().S(`:`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:70
|
||||
streamprocessJson(qw422016, trace.processMap[0].process)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:71
|
||||
for _, v := range trace.processMap[1:] {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:71
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:72
|
||||
qw422016.N().Q(v.processID)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:72
|
||||
qw422016.N().S(`:`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:72
|
||||
streamprocessJson(qw422016, v.process)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:73
|
||||
}
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:74
|
||||
}
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:74
|
||||
qw422016.N().S(`},"spans": [`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:77
|
||||
if len(trace.spans) > 0 {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:78
|
||||
streamspanJson(qw422016, trace.spans[0])
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:79
|
||||
for _, v := range trace.spans[1:] {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:79
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:80
|
||||
streamspanJson(qw422016, v)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:81
|
||||
}
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:82
|
||||
}
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:82
|
||||
qw422016.N().S(`],"traceID":`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:84
|
||||
qw422016.N().Q(trace.spans[0].traceID)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:84
|
||||
qw422016.N().S(`,"warnings": null}`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:87
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:87
|
||||
func writetraceJson(qq422016 qtio422016.Writer, trace *trace) {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:87
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:87
|
||||
streamtraceJson(qw422016, trace)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:87
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:87
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:87
|
||||
func traceJson(trace *trace) string {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:87
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:87
|
||||
writetraceJson(qb422016, trace)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:87
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:87
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:87
|
||||
return qs422016
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:87
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:89
|
||||
func streamprocessJson(qw422016 *qt422016.Writer, process process) {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:89
|
||||
qw422016.N().S(`{"serviceName":`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:91
|
||||
qw422016.N().Q(process.serviceName)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:91
|
||||
qw422016.N().S(`,"tags": [`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:93
|
||||
if len(process.tags) > 0 {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:94
|
||||
streamtagJson(qw422016, process.tags[0])
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:95
|
||||
for _, v := range process.tags[1:] {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:95
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:96
|
||||
streamtagJson(qw422016, v)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:97
|
||||
}
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:98
|
||||
}
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:98
|
||||
qw422016.N().S(`]}`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:101
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:101
|
||||
func writeprocessJson(qq422016 qtio422016.Writer, process process) {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:101
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:101
|
||||
streamprocessJson(qw422016, process)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:101
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:101
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:101
|
||||
func processJson(process process) string {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:101
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:101
|
||||
writeprocessJson(qb422016, process)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:101
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:101
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:101
|
||||
return qs422016
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:101
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:103
|
||||
func streamspanJson(qw422016 *qt422016.Writer, span *span) {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:103
|
||||
qw422016.N().S(`{"duration":`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:105
|
||||
qw422016.N().DL(span.duration)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:105
|
||||
qw422016.N().S(`,"logs":[`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:107
|
||||
if len(span.logs) > 0 {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:108
|
||||
streamlogJson(qw422016, span.logs[0])
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:109
|
||||
for _, v := range span.logs[1:] {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:109
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:110
|
||||
streamlogJson(qw422016, v)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:111
|
||||
}
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:112
|
||||
}
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:112
|
||||
qw422016.N().S(`],"operationName":`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:114
|
||||
qw422016.N().Q(span.operationName)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:114
|
||||
qw422016.N().S(`,"processID":`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:115
|
||||
qw422016.N().Q(span.processID)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:115
|
||||
qw422016.N().S(`,"references": [`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:117
|
||||
if len(span.references) > 0 {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:118
|
||||
streamspanRefJson(qw422016, span.references[0])
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:119
|
||||
for _, v := range span.references[1:] {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:119
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:120
|
||||
streamspanRefJson(qw422016, v)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:121
|
||||
}
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:122
|
||||
}
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:122
|
||||
qw422016.N().S(`],"spanID":`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:124
|
||||
qw422016.N().Q(span.spanID)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:124
|
||||
qw422016.N().S(`,"startTime":`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:125
|
||||
qw422016.N().DL(span.startTime)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:125
|
||||
qw422016.N().S(`,"tags": [`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:127
|
||||
if len(span.tags) > 0 {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:128
|
||||
streamtagJson(qw422016, span.tags[0])
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:129
|
||||
for _, v := range span.tags[1:] {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:129
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:130
|
||||
streamtagJson(qw422016, v)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:131
|
||||
}
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:132
|
||||
}
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:132
|
||||
qw422016.N().S(`],"traceID":`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:134
|
||||
qw422016.N().Q(span.traceID)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:134
|
||||
qw422016.N().S(`,"warnings":null}`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:137
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:137
|
||||
func writespanJson(qq422016 qtio422016.Writer, span *span) {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:137
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:137
|
||||
streamspanJson(qw422016, span)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:137
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:137
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:137
|
||||
func spanJson(span *span) string {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:137
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:137
|
||||
writespanJson(qb422016, span)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:137
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:137
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:137
|
||||
return qs422016
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:137
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:139
|
||||
func streamtagJson(qw422016 *qt422016.Writer, tag keyValue) {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:139
|
||||
qw422016.N().S(`{"key":`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:141
|
||||
qw422016.N().Q(tag.key)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:141
|
||||
qw422016.N().S(`,"type":"string","value":`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:143
|
||||
qw422016.N().Q(tag.vStr)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:143
|
||||
qw422016.N().S(`}`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:145
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:145
|
||||
func writetagJson(qq422016 qtio422016.Writer, tag keyValue) {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:145
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:145
|
||||
streamtagJson(qw422016, tag)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:145
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:145
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:145
|
||||
func tagJson(tag keyValue) string {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:145
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:145
|
||||
writetagJson(qb422016, tag)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:145
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:145
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:145
|
||||
return qs422016
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:145
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:147
|
||||
func streamlogJson(qw422016 *qt422016.Writer, l log) {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:147
|
||||
qw422016.N().S(`{"timestamp":`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:149
|
||||
qw422016.N().DL(l.timestamp)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:149
|
||||
qw422016.N().S(`,"fields":[`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:151
|
||||
if len(l.fields) > 0 {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:152
|
||||
streamtagJson(qw422016, l.fields[0])
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:153
|
||||
for _, v := range l.fields[1:] {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:153
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:154
|
||||
streamtagJson(qw422016, v)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:155
|
||||
}
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:156
|
||||
}
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:156
|
||||
qw422016.N().S(`]}`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:159
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:159
|
||||
func writelogJson(qq422016 qtio422016.Writer, l log) {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:159
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:159
|
||||
streamlogJson(qw422016, l)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:159
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:159
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:159
|
||||
func logJson(l log) string {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:159
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:159
|
||||
writelogJson(qb422016, l)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:159
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:159
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:159
|
||||
return qs422016
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:159
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:161
|
||||
func streamspanRefJson(qw422016 *qt422016.Writer, ref spanRef) {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:161
|
||||
qw422016.N().S(`{"refType":`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:163
|
||||
qw422016.N().Q(ref.refType)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:163
|
||||
qw422016.N().S(`,"spanID":`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:164
|
||||
qw422016.N().Q(ref.spanID)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:164
|
||||
qw422016.N().S(`,"traceID":`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:165
|
||||
qw422016.N().Q(ref.traceID)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:165
|
||||
qw422016.N().S(`}`)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:167
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:167
|
||||
func writespanRefJson(qq422016 qtio422016.Writer, ref spanRef) {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:167
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:167
|
||||
streamspanRefJson(qw422016, ref)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:167
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:167
|
||||
}
|
||||
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:167
|
||||
func spanRefJson(ref spanRef) string {
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:167
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:167
|
||||
writespanRefJson(qb422016, ref)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:167
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:167
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:167
|
||||
return qs422016
|
||||
//line app/vlselect/traces/jaeger/jaeger.qtpl:167
|
||||
}
|
||||
274
app/vlselect/traces/jaeger/model.go
Normal file
274
app/vlselect/traces/jaeger/model.go
Normal file
@@ -0,0 +1,274 @@
|
||||
package jaeger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
|
||||
otelpb "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/pb"
|
||||
)
|
||||
|
||||
type trace struct {
|
||||
spans []*span
|
||||
processMap []processMap
|
||||
}
|
||||
|
||||
type processMap struct {
|
||||
processID string
|
||||
process process
|
||||
}
|
||||
|
||||
type process struct {
|
||||
serviceName string
|
||||
tags []keyValue
|
||||
}
|
||||
|
||||
type span struct {
|
||||
traceID string
|
||||
spanID string
|
||||
operationName string
|
||||
references []spanRef
|
||||
//flags uint32 // OTLP - jaeger conversion does not use this field, but it exists in jaeger definition.
|
||||
startTime int64
|
||||
duration int64
|
||||
tags []keyValue
|
||||
logs []log
|
||||
process process
|
||||
processID string
|
||||
//warnings []string // OTLP - jaeger conversion does not use this field, but it exists in jaeger definition.
|
||||
}
|
||||
|
||||
type spanRef struct {
|
||||
traceID string
|
||||
spanID string
|
||||
refType string
|
||||
}
|
||||
|
||||
type keyValue struct {
|
||||
key string
|
||||
vStr string
|
||||
}
|
||||
|
||||
type log struct {
|
||||
timestamp int64
|
||||
fields []keyValue
|
||||
}
|
||||
|
||||
// since Jaeger renamed some fields in OpenTelemetry
|
||||
// into other span attributes during query, the following map
|
||||
// is created to translate the span attributes filter into the
|
||||
// original field names in OpenTelemetry (VictoriaTraces).
|
||||
//
|
||||
// format: <special span attributes in Jaeger>: <fields in OpenTelemetry>
|
||||
var spanAttributeMap = map[string]string{
|
||||
// special cases that need to map string to int status code, see errorStatusCodeMap
|
||||
"error": otelpb.StatusCodeField,
|
||||
"span.kind": otelpb.KindField,
|
||||
|
||||
// only attributes/field name conversion.
|
||||
"otel.status_description": otelpb.StatusMessageField,
|
||||
"w3c.tracestate": otelpb.TraceStateField,
|
||||
"otel.scope.name": otelpb.InstrumentationScopeName,
|
||||
"otel.scope.version": otelpb.InstrumentationScopeVersion,
|
||||
// scope attributes
|
||||
}
|
||||
|
||||
var errorStatusCodeMap = map[string]string{
|
||||
"unset": "0",
|
||||
"true": "2",
|
||||
"false": "1",
|
||||
}
|
||||
|
||||
var spanKindMap = map[string]string{
|
||||
"internal": "1",
|
||||
"server": "2",
|
||||
"client": "3",
|
||||
"producer": "4",
|
||||
"consumer": "5",
|
||||
}
|
||||
|
||||
// fieldsToSpan convert OTLP spans in fields to Jaeger Spans.
|
||||
func fieldsToSpan(fields []logstorage.Field) (*span, error) {
|
||||
sp := &span{}
|
||||
|
||||
processTagList, spanTagList := make([]keyValue, 0, len(fields)), make([]keyValue, 0, len(fields))
|
||||
logsMap := make(map[string]*log) // idx -> *Log
|
||||
refsMap := make(map[string]*spanRef) // idx -> *SpanRef
|
||||
|
||||
parentSpanRef := spanRef{}
|
||||
for _, field := range fields {
|
||||
switch field.Name {
|
||||
case "_stream":
|
||||
// no-op
|
||||
case otelpb.TraceIDField:
|
||||
sp.traceID = field.Value
|
||||
case otelpb.SpanIDField:
|
||||
sp.spanID = field.Value
|
||||
case otelpb.NameField:
|
||||
sp.operationName = field.Value
|
||||
case otelpb.ParentSpanIDField:
|
||||
parentSpanRef.spanID = field.Value
|
||||
parentSpanRef.refType = "CHILD_OF"
|
||||
case otelpb.KindField:
|
||||
if field.Value != "" {
|
||||
spanKind := ""
|
||||
switch field.Value {
|
||||
case "1":
|
||||
spanKind = "internal"
|
||||
case "2":
|
||||
spanKind = "server"
|
||||
case "3":
|
||||
spanKind = "client"
|
||||
case "4":
|
||||
spanKind = "producer"
|
||||
case "5":
|
||||
spanKind = "consumer"
|
||||
default:
|
||||
// unexpected span kind.
|
||||
// this line does nothing should never be reached.
|
||||
}
|
||||
spanTagList = append(spanTagList, keyValue{key: "span.kind", vStr: spanKind})
|
||||
}
|
||||
case otelpb.FlagsField:
|
||||
// todo trace does not contain "flag" in result
|
||||
//flagU64, err := strconv.ParseUint(field.Value, 10, 32)
|
||||
//if err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
//sp.Flags = uint32(flagU64)
|
||||
case otelpb.StartTimeUnixNanoField:
|
||||
unixNano, err := strconv.ParseInt(field.Value, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid start_time_unix_nano field: %s", err)
|
||||
}
|
||||
sp.startTime = unixNano / 1000
|
||||
case otelpb.DurationField:
|
||||
nano, err := strconv.ParseInt(field.Value, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid duration field: %s", err)
|
||||
}
|
||||
sp.duration = nano / 1000
|
||||
case otelpb.StatusCodeField:
|
||||
v := "unset"
|
||||
switch field.Value {
|
||||
case "1":
|
||||
v = "false"
|
||||
case "2":
|
||||
v = "true"
|
||||
}
|
||||
spanTagList = append(spanTagList, keyValue{key: "error", vStr: v})
|
||||
case otelpb.StatusMessageField:
|
||||
if field.Value != "" {
|
||||
spanTagList = append(spanTagList, keyValue{key: "otel.status_description", vStr: field.Value})
|
||||
}
|
||||
case otelpb.TraceStateField:
|
||||
if field.Value != "" {
|
||||
spanTagList = append(spanTagList, keyValue{key: "w3c.tracestate", vStr: field.Value})
|
||||
}
|
||||
// resource level fields
|
||||
case otelpb.ResourceAttrServiceName:
|
||||
sp.process.serviceName = field.Value
|
||||
// scope level fields
|
||||
case otelpb.InstrumentationScopeName:
|
||||
if field.Value != "" {
|
||||
spanTagList = append(spanTagList, keyValue{key: "otel.scope.name", vStr: field.Value})
|
||||
}
|
||||
case otelpb.InstrumentationScopeVersion:
|
||||
if field.Value != "" {
|
||||
spanTagList = append(spanTagList, keyValue{key: "otel.scope.version", vStr: field.Value})
|
||||
}
|
||||
default:
|
||||
if strings.HasPrefix(field.Name, otelpb.ResourceAttrPrefix) { // resource attributes
|
||||
processTagList = append(processTagList, keyValue{key: strings.TrimPrefix(field.Name, otelpb.ResourceAttrPrefix), vStr: field.Value})
|
||||
} else if strings.HasPrefix(field.Name, otelpb.SpanAttrPrefixField) { // span attributes
|
||||
spanTagList = append(spanTagList, keyValue{key: strings.TrimPrefix(field.Name, otelpb.SpanAttrPrefixField), vStr: field.Value})
|
||||
} else if strings.HasPrefix(field.Name, otelpb.InstrumentationScopeAttrPrefix) { // instrumentation scope attributes
|
||||
// we have to display `scope_attr:` prefix as there's no way to distinguish these from span attributes.
|
||||
spanTagList = append(spanTagList, keyValue{key: field.Name, vStr: field.Value})
|
||||
} else if strings.HasPrefix(field.Name, otelpb.EventPrefix) { // event list
|
||||
fieldSplit := strings.SplitN(strings.TrimPrefix(field.Name, otelpb.EventPrefix), ":", 2)
|
||||
if len(fieldSplit) != 2 {
|
||||
return nil, fmt.Errorf("invalid event field: %s", field.Name)
|
||||
}
|
||||
idx, fieldName := fieldSplit[0], fieldSplit[1]
|
||||
if _, ok := logsMap[idx]; !ok {
|
||||
logsMap[idx] = &log{}
|
||||
}
|
||||
lg := logsMap[idx]
|
||||
switch fieldName {
|
||||
case otelpb.EventTimeUnixNanoField:
|
||||
unixNano, _ := strconv.ParseInt(field.Value, 10, 64)
|
||||
lg.timestamp = unixNano / 1000
|
||||
case otelpb.EventNameField:
|
||||
lg.fields = append(lg.fields, keyValue{key: "event", vStr: field.Value})
|
||||
case otelpb.EventDroppedAttributesCountField:
|
||||
//no need to display
|
||||
//lg.Fields = append(lg.Fields, KeyValue{Key: fieldName, VStr: field.Value})
|
||||
default:
|
||||
lg.fields = append(lg.fields, keyValue{key: strings.TrimPrefix(fieldName, otelpb.EventAttrPrefix), vStr: field.Value})
|
||||
}
|
||||
} else if strings.HasPrefix(field.Name, otelpb.LinkPrefix) { // link list
|
||||
fieldSplit := strings.SplitN(strings.TrimPrefix(field.Name, otelpb.LinkPrefix), ":", 2)
|
||||
if len(fieldSplit) != 2 {
|
||||
return nil, fmt.Errorf("invalid link field: %s", field.Name)
|
||||
}
|
||||
idx, fieldName := fieldSplit[0], fieldSplit[1]
|
||||
if _, ok := refsMap[idx]; !ok {
|
||||
refsMap[idx] = &spanRef{
|
||||
refType: "FOLLOWS_FROM", // default FOLLOWS_FROM
|
||||
}
|
||||
}
|
||||
ref := refsMap[idx]
|
||||
switch fieldName {
|
||||
case otelpb.LinkTraceIDField:
|
||||
ref.traceID = field.Value
|
||||
case otelpb.LinkSpanIDField:
|
||||
ref.spanID = field.Value
|
||||
case otelpb.LinkTraceStateField, otelpb.LinkFlagsField, otelpb.LinkDroppedAttributesCountField:
|
||||
default:
|
||||
if strings.TrimPrefix(fieldName, otelpb.LinkAttrPrefix) == "opentracing.ref_type" && field.Value == "child_of" {
|
||||
ref.refType = "CHILD_OF" // CHILD_OF
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sp.spanID == "" || sp.traceID == "" {
|
||||
return nil, fmt.Errorf("invalid fields: %v", fields)
|
||||
}
|
||||
|
||||
if len(spanTagList) > 0 {
|
||||
sp.tags = spanTagList
|
||||
}
|
||||
|
||||
if len(processTagList) > 0 {
|
||||
sp.process.tags = processTagList
|
||||
}
|
||||
|
||||
if parentSpanRef.spanID != "" {
|
||||
parentSpanRef.traceID = sp.traceID
|
||||
sp.references = append(sp.references, parentSpanRef)
|
||||
}
|
||||
|
||||
for i := 0; i < len(refsMap); i++ {
|
||||
idx := strconv.Itoa(i)
|
||||
if len(sp.references) > 0 && parentSpanRef.traceID == refsMap[idx].traceID && parentSpanRef.spanID == refsMap[idx].spanID {
|
||||
// We already added a reference to this span, but maybe with the wrong type, so override.
|
||||
sp.references[0].refType = refsMap[idx].refType
|
||||
continue
|
||||
}
|
||||
sp.references = append(sp.references, spanRef{
|
||||
refsMap[idx].traceID, refsMap[idx].spanID, refsMap[idx].refType,
|
||||
})
|
||||
}
|
||||
for i := 0; i < len(logsMap); i++ {
|
||||
idx := strconv.Itoa(i)
|
||||
sp.logs = append(sp.logs, log{
|
||||
logsMap[idx].timestamp, logsMap[idx].fields,
|
||||
})
|
||||
}
|
||||
|
||||
return sp, nil
|
||||
}
|
||||
164
app/vlselect/traces/jaeger/model_test.go
Normal file
164
app/vlselect/traces/jaeger/model_test.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package jaeger
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
|
||||
otelpb "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/pb"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestFieldsToSpan(t *testing.T) {
|
||||
f := func(input []logstorage.Field, want *span, errorMsg string) {
|
||||
t.Helper()
|
||||
|
||||
var errMsgGot string
|
||||
got, err := fieldsToSpan(input)
|
||||
if err != nil {
|
||||
errMsgGot = err.Error()
|
||||
}
|
||||
if errMsgGot != errorMsg {
|
||||
t.Fatalf("fieldsToSpan() error = %v, want err: %v", err, errorMsg)
|
||||
}
|
||||
cmpOpts := cmp.AllowUnexported(span{}, process{}, spanRef{}, keyValue{}, log{})
|
||||
if !cmp.Equal(got, want, cmpOpts) {
|
||||
t.Fatalf("fieldsToSpan() diff = %v", cmp.Diff(got, want, cmpOpts))
|
||||
}
|
||||
}
|
||||
|
||||
// case 1: empty
|
||||
f([]logstorage.Field{}, nil, "invalid fields: []")
|
||||
|
||||
// case 2: without span_id
|
||||
fields := []logstorage.Field{
|
||||
{Name: otelpb.TraceIDField, Value: "1234567890"},
|
||||
}
|
||||
f(fields, nil, "invalid fields: [{trace_id 1234567890}]")
|
||||
|
||||
// case 3: without trace_id
|
||||
fields = []logstorage.Field{
|
||||
{Name: otelpb.SpanIDField, Value: "12345"},
|
||||
}
|
||||
f(fields, nil, "invalid fields: [{span_id 12345}]")
|
||||
|
||||
// case 4: with basic fields
|
||||
fields = []logstorage.Field{
|
||||
{Name: otelpb.TraceIDField, Value: "1234567890"},
|
||||
{Name: otelpb.SpanIDField, Value: "12345"},
|
||||
}
|
||||
sp := &span{
|
||||
traceID: "1234567890", spanID: "12345",
|
||||
}
|
||||
f(fields, sp, "")
|
||||
|
||||
// case 5: with all fields
|
||||
// see: lib/protoparser/opentelemetry/pb/trace_fields.go
|
||||
fields = []logstorage.Field{
|
||||
{Name: otelpb.ResourceAttrServiceName, Value: "service_name_1"},
|
||||
{Name: otelpb.ResourceAttrPrefix + "resource_attr_1", Value: "resource_attr_1"},
|
||||
{Name: otelpb.ResourceAttrPrefix + "resource_attr_2", Value: "resource_attr_2"},
|
||||
|
||||
{Name: otelpb.InstrumentationScopeName, Value: "scope_name_1"},
|
||||
{Name: otelpb.InstrumentationScopeVersion, Value: "scope_version_1"},
|
||||
{Name: otelpb.InstrumentationScopeAttrPrefix + "scope_attr_1", Value: "scope_attr_1"},
|
||||
{Name: otelpb.InstrumentationScopeAttrPrefix + "scope_attr_2", Value: "scope_attr_2"},
|
||||
|
||||
{Name: otelpb.TraceIDField, Value: "1234567890"},
|
||||
{Name: otelpb.SpanIDField, Value: "12345"},
|
||||
{Name: otelpb.TraceStateField, Value: "trace_state_1"},
|
||||
{Name: otelpb.ParentSpanIDField, Value: "23456"},
|
||||
{Name: otelpb.FlagsField, Value: "0"},
|
||||
{Name: otelpb.NameField, Value: "span_name_1"},
|
||||
{Name: otelpb.KindField, Value: "1"},
|
||||
{Name: otelpb.StartTimeUnixNanoField, Value: "0"},
|
||||
{Name: otelpb.EndTimeUnixNanoField, Value: "123456789"},
|
||||
{Name: otelpb.SpanAttrPrefixField + "attr_1", Value: "attr_1"},
|
||||
{Name: otelpb.SpanAttrPrefixField + "attr_2", Value: "attr_2"},
|
||||
{Name: otelpb.DurationField, Value: "123456789"},
|
||||
|
||||
{Name: otelpb.EventPrefix + "0:" + otelpb.EventTimeUnixNanoField, Value: "0"},
|
||||
{Name: otelpb.EventPrefix + "0:" + otelpb.EventNameField, Value: "event_0"},
|
||||
{Name: otelpb.EventPrefix + "0:" + otelpb.EventAttrPrefix + "event_attr_1", Value: "event_0_attr_1"},
|
||||
{Name: otelpb.EventPrefix + "0:" + otelpb.EventAttrPrefix + "event_attr_2", Value: "event_0_attr_2"},
|
||||
|
||||
{Name: otelpb.EventPrefix + "1:" + otelpb.EventTimeUnixNanoField, Value: "1"},
|
||||
{Name: otelpb.EventPrefix + "1:" + otelpb.EventNameField, Value: "event_1"},
|
||||
{Name: otelpb.EventPrefix + "1:" + otelpb.EventAttrPrefix + "event_attr_1", Value: "event_1_attr_1"},
|
||||
{Name: otelpb.EventPrefix + "1:" + otelpb.EventAttrPrefix + "event_attr_2", Value: "event_1_attr_2"},
|
||||
|
||||
{Name: otelpb.LinkPrefix + "0:" + otelpb.LinkTraceIDField, Value: "1234567890"},
|
||||
{Name: otelpb.LinkPrefix + "0:" + otelpb.LinkSpanIDField, Value: "23456"},
|
||||
{Name: otelpb.LinkPrefix + "0:" + otelpb.LinkTraceStateField, Value: "link_0_trace_state_1"},
|
||||
{Name: otelpb.LinkPrefix + "0:" + otelpb.LinkAttrPrefix + "link_attr_1", Value: "link_0_trace_attr_1"},
|
||||
{Name: otelpb.LinkPrefix + "0:" + otelpb.LinkAttrPrefix + "link_attr_2", Value: "link_0_trace_attr_2"},
|
||||
{Name: otelpb.LinkPrefix + "0:" + otelpb.LinkAttrPrefix + "opentracing.ref_type", Value: "child_of"},
|
||||
{Name: otelpb.LinkPrefix + "0:" + otelpb.LinkFlagsField, Value: "0"},
|
||||
{Name: otelpb.LinkPrefix + "1:" + otelpb.LinkTraceIDField, Value: "99999999999"},
|
||||
{Name: otelpb.LinkPrefix + "1:" + otelpb.LinkSpanIDField, Value: "98765"},
|
||||
{Name: otelpb.LinkPrefix + "1:" + otelpb.LinkTraceStateField, Value: "link_1_trace_state_1"},
|
||||
{Name: otelpb.LinkPrefix + "1:" + otelpb.LinkAttrPrefix + "link_attr_1", Value: "link_1_trace_attr_1"},
|
||||
{Name: otelpb.LinkPrefix + "1:" + otelpb.LinkAttrPrefix + "link_attr_2", Value: "link_1_trace_attr_2"},
|
||||
{Name: otelpb.LinkPrefix + "1:" + otelpb.LinkFlagsField, Value: "1"},
|
||||
|
||||
{Name: otelpb.StatusMessageField, Value: "status_message_1"},
|
||||
{Name: otelpb.StatusCodeField, Value: "2"},
|
||||
}
|
||||
|
||||
sp = &span{
|
||||
traceID: "1234567890",
|
||||
spanID: "12345",
|
||||
operationName: "span_name_1",
|
||||
references: []spanRef{
|
||||
{
|
||||
traceID: "1234567890",
|
||||
spanID: "23456",
|
||||
refType: "CHILD_OF",
|
||||
},
|
||||
{
|
||||
traceID: "99999999999",
|
||||
spanID: "98765",
|
||||
refType: "FOLLOWS_FROM",
|
||||
},
|
||||
},
|
||||
startTime: 0,
|
||||
duration: 123456,
|
||||
tags: []keyValue{
|
||||
{"otel.scope.name", "scope_name_1"},
|
||||
{"otel.scope.version", "scope_version_1"},
|
||||
{"scope_attr:scope_attr_1", "scope_attr_1"},
|
||||
{"scope_attr:scope_attr_2", "scope_attr_2"},
|
||||
{"w3c.tracestate", "trace_state_1"},
|
||||
{"span.kind", "internal"},
|
||||
{"attr_1", "attr_1"},
|
||||
{"attr_2", "attr_2"},
|
||||
{"otel.status_description", "status_message_1"},
|
||||
{"error", "true"},
|
||||
},
|
||||
logs: []log{
|
||||
{
|
||||
timestamp: 0,
|
||||
fields: []keyValue{
|
||||
{"event", "event_0"},
|
||||
{"event_attr_1", "event_0_attr_1"},
|
||||
{"event_attr_2", "event_0_attr_2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamp: 0,
|
||||
fields: []keyValue{
|
||||
{"event", "event_1"},
|
||||
{"event_attr_1", "event_1_attr_1"},
|
||||
{"event_attr_2", "event_1_attr_2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
process: process{
|
||||
serviceName: "service_name_1",
|
||||
tags: []keyValue{
|
||||
{"resource_attr_1", "resource_attr_1"},
|
||||
{"resource_attr_2", "resource_attr_2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
f(fields, sp, "")
|
||||
}
|
||||
430
app/vlselect/traces/query/query.go
Normal file
430
app/vlselect/traces/query/query.go
Normal file
@@ -0,0 +1,430 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlstorage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
|
||||
otelpb "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/pb"
|
||||
)
|
||||
|
||||
var (
|
||||
traceMaxDurationWindow = flag.Duration("search.traceMaxDurationWindow", 10*time.Minute, "The window of searching for the rest trace spans after finding one span."+
|
||||
"It allows extending the search start time and end time by `-search.traceMaxDurationWindow` to make sure all spans are included."+
|
||||
"It affects both Jaeger's `/api/traces` and `/api/traces/<trace_id>` APIs.")
|
||||
traceServiceAndSpanNameLookbehind = flag.Duration("search.traceServiceAndSpanNameLookbehind", 7*24*time.Hour, "The time range of searching for service name and span name. "+
|
||||
"It affects Jaeger's `/api/services` and `/api/services/*/operations` APIs.")
|
||||
traceSearchStep = flag.Duration("search.traceSearchStep", 24*time.Hour, "Splits the [0, now] time range into many small time ranges by -search.traceSearchStep "+
|
||||
"when searching for spans by trace_id. Once it finds spans in a time range, it performs an additional search according to -search.traceMaxDurationWindow and then stops. "+
|
||||
"It affects Jaeger's `/api/traces/<trace_id>` API.")
|
||||
traceMaxServiceNameList = flag.Uint64("search.traceMaxServiceNameList", 1000, "The maximum number of service name can return in a get service name request. "+
|
||||
"This limit affects Jaeger's `/api/services` API.")
|
||||
traceMaxSpanNameList = flag.Uint64("search.traceMaxSpanNameList", 1000, "The maximum number of span name can return in a get span name request. "+
|
||||
"This limit affects Jaeger's `/api/services/*/operations` API.")
|
||||
)
|
||||
|
||||
var (
|
||||
traceIDRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.:]*$`)
|
||||
)
|
||||
|
||||
// CommonParams common query params that shared by all requests.
|
||||
type CommonParams struct {
|
||||
TenantIDs []logstorage.TenantID
|
||||
}
|
||||
|
||||
// GetCommonParams get common params from request for all traces query APIs.
|
||||
func GetCommonParams(r *http.Request) (*CommonParams, error) {
|
||||
tenantID, err := logstorage.GetTenantIDFromRequest(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot obtain tenanID: %w", err)
|
||||
}
|
||||
tenantIDs := []logstorage.TenantID{tenantID}
|
||||
cp := &CommonParams{
|
||||
TenantIDs: tenantIDs,
|
||||
}
|
||||
return cp, nil
|
||||
}
|
||||
|
||||
// TraceQueryParam is the parameters for querying a batch of traces.
|
||||
type TraceQueryParam struct {
|
||||
ServiceName string
|
||||
SpanName string
|
||||
Attributes map[string]string
|
||||
StartTimeMin time.Time
|
||||
StartTimeMax time.Time
|
||||
DurationMin time.Duration
|
||||
DurationMax time.Duration
|
||||
Limit int
|
||||
}
|
||||
|
||||
// Row represent the query result of a trace span.
|
||||
type Row struct {
|
||||
Timestamp int64
|
||||
Fields []logstorage.Field
|
||||
}
|
||||
|
||||
// GetServiceNameList returns all unique service names within *traceServiceAndSpanNameLookbehind window.
|
||||
// todo: cache of recent result.
|
||||
func GetServiceNameList(ctx context.Context, cp *CommonParams) ([]string, error) {
|
||||
currentTime := time.Now()
|
||||
|
||||
// query: _time:[start, end] *
|
||||
qStr := "*"
|
||||
q, err := logstorage.ParseQueryAtTimestamp(qStr, currentTime.UnixNano())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse query [%s]: %s", qStr, err)
|
||||
}
|
||||
q.AddTimeFilter(currentTime.Add(-*traceServiceAndSpanNameLookbehind).UnixNano(), currentTime.UnixNano())
|
||||
|
||||
serviceHits, err := vlstorage.GetStreamFieldValues(ctx, cp.TenantIDs, q, otelpb.ResourceAttrServiceName, *traceMaxServiceNameList)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse query [%s]: %s", qStr, err)
|
||||
}
|
||||
|
||||
serviceList := make([]string, 0, len(serviceHits))
|
||||
for i := range serviceHits {
|
||||
serviceList = append(serviceList, serviceHits[i].Value)
|
||||
}
|
||||
return serviceList, nil
|
||||
}
|
||||
|
||||
// GetSpanNameList returns all unique span names for a service within *traceServiceAndSpanNameLookbehind window.
|
||||
// todo: cache of recent result.
|
||||
func GetSpanNameList(ctx context.Context, cp *CommonParams, serviceName string) ([]string, error) {
|
||||
currentTime := time.Now()
|
||||
|
||||
// query: _time:[start, end] {"resource_attr:service.name"=serviceName}
|
||||
qStr := fmt.Sprintf("_stream:{%s=%q}", otelpb.ResourceAttrServiceName, serviceName)
|
||||
q, err := logstorage.ParseQueryAtTimestamp(qStr, currentTime.Unix())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse query [%s]: %s", qStr, err)
|
||||
}
|
||||
q.AddTimeFilter(currentTime.Add(-*traceServiceAndSpanNameLookbehind).UnixNano(), currentTime.UnixNano())
|
||||
|
||||
spanNameHits, err := vlstorage.GetStreamFieldValues(ctx, cp.TenantIDs, q, otelpb.NameField, *traceMaxSpanNameList)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get span name hits error: %s", err)
|
||||
}
|
||||
|
||||
spanNameList := make([]string, 0, len(spanNameHits))
|
||||
for i := range spanNameHits {
|
||||
spanNameList = append(spanNameList, spanNameHits[i].Value)
|
||||
}
|
||||
return spanNameList, nil
|
||||
}
|
||||
|
||||
// GetTrace returns all spans of a trace in []*Row format.
|
||||
// In order to avoid scanning all data blocks, search is performed on time range splitting by traceSearchStep.
|
||||
// Once a trace is found, it assumes other spans will exist on the same time range, and only search this
|
||||
// time range (with traceMaxDurationWindow).
|
||||
//
|
||||
// e.g.
|
||||
// 1. find traces span on [now-traceSearchStep, now], no hit.
|
||||
// 2. find traces span on [now-2 * traceSearchStep, now - traceSearchStep], hit.
|
||||
// 3. make sure to include all the spans by an additional search on: [now-2 * traceSearchStep-traceMaxDurationWindow, now-2 * traceSearchStep].
|
||||
// 4. skip [0, now-2 * traceSearchStep-traceMaxDurationWindow] and return.
|
||||
//
|
||||
// todo in-memory cache of hot traces.
|
||||
func GetTrace(ctx context.Context, cp *CommonParams, traceID string) ([]*Row, error) {
|
||||
currentTime := time.Now()
|
||||
|
||||
// query: trace_id:traceID
|
||||
qStr := fmt.Sprintf(otelpb.TraceIDField+": %q", traceID)
|
||||
q, err := logstorage.ParseQueryAtTimestamp(qStr, currentTime.UnixNano())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse query [%s]: %s", qStr, err)
|
||||
}
|
||||
|
||||
ctxWithCancel, cancel := context.WithCancel(ctx)
|
||||
|
||||
// search for trace spans and write to `rows []*Row`
|
||||
var rowsLock sync.Mutex
|
||||
var rows []*Row
|
||||
var missingTimeColumn atomic.Bool
|
||||
writeBlock := func(_ uint, db *logstorage.DataBlock) {
|
||||
if missingTimeColumn.Load() {
|
||||
return
|
||||
}
|
||||
|
||||
columns := db.Columns
|
||||
clonedColumnNames := make([]string, len(columns))
|
||||
for i, c := range columns {
|
||||
clonedColumnNames[i] = strings.Clone(c.Name)
|
||||
}
|
||||
|
||||
timestamps, ok := db.GetTimestamps(nil)
|
||||
if !ok {
|
||||
missingTimeColumn.Store(true)
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
|
||||
for i, timestamp := range timestamps {
|
||||
fields := make([]logstorage.Field, 0, len(columns))
|
||||
for j := range columns {
|
||||
// column could be empty if this span does not contain such field.
|
||||
// only append non-empty columns.
|
||||
if columns[j].Values[i] != "" {
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: clonedColumnNames[j],
|
||||
Value: strings.Clone(columns[j].Values[i]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
rowsLock.Lock()
|
||||
rows = append(rows, &Row{
|
||||
Timestamp: timestamp,
|
||||
Fields: fields,
|
||||
})
|
||||
rowsLock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
startTime := currentTime.Add(-*traceSearchStep)
|
||||
endTime := currentTime
|
||||
for startTime.UnixNano() > 0 { // todo: no need to search time range before retention period.
|
||||
qq := q.CloneWithTimeFilter(currentTime.UnixNano(), startTime.UnixNano(), endTime.UnixNano())
|
||||
if err = vlstorage.RunQuery(ctxWithCancel, cp.TenantIDs, qq, writeBlock); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if missingTimeColumn.Load() {
|
||||
return nil, fmt.Errorf("missing _time column in the result for the query [%s]", qq)
|
||||
}
|
||||
|
||||
// no hit in this time range, continue with step.
|
||||
if len(rows) == 0 {
|
||||
endTime = startTime
|
||||
startTime = startTime.Add(-*traceSearchStep)
|
||||
continue
|
||||
}
|
||||
|
||||
// found result, perform extra search for traceMaxDurationWindow and then break.
|
||||
qq = q.CloneWithTimeFilter(currentTime.UnixNano(), startTime.Add(-*traceMaxDurationWindow).UnixNano(), startTime.UnixNano())
|
||||
if err = vlstorage.RunQuery(ctxWithCancel, cp.TenantIDs, qq, writeBlock); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if missingTimeColumn.Load() {
|
||||
return nil, fmt.Errorf("missing _time column in the result for the query [%s]", qq)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
// GetTraceList returns multiple traceIDs and spans of them in []*Row format.
|
||||
// It search for traceIDs first, and then search for the spans of these traceIDs.
|
||||
// To not miss any spans on the edge, it extends both the start time and end time
|
||||
// by *traceMaxDurationWindow.
|
||||
//
|
||||
// e.g.:
|
||||
// 1. input time range: [00:00, 09:00]
|
||||
// 2. found 20 trace id, and adjust time range to: [08:00, 09:00]
|
||||
// 3. find spans on time range: [08:00-traceMaxDurationWindow, 09:00+traceMaxDurationWindow]
|
||||
func GetTraceList(ctx context.Context, cp *CommonParams, param *TraceQueryParam) ([]string, []*Row, error) {
|
||||
currentTime := time.Now()
|
||||
|
||||
// query 1: * AND filter_conditions | last 1 by (_time) partition by (trace_id) | fields _time, trace_id | sort by (_time) desc
|
||||
traceIDs, startTime, err := getTraceIDList(ctx, cp, param)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("get trace id error: %w", err)
|
||||
}
|
||||
if len(traceIDs) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// query 2: trace_id:in(traceID, traceID, ...)
|
||||
qStr := fmt.Sprintf(otelpb.TraceIDField+":in(%s)", strings.Join(traceIDs, ","))
|
||||
q, err := logstorage.ParseQueryAtTimestamp(qStr, currentTime.UnixNano())
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot parse query [%s]: %s", qStr, err)
|
||||
}
|
||||
|
||||
// adjust start time and end time with max duration window to make sure all spans are included.
|
||||
q.AddTimeFilter(startTime.Add(-*traceMaxDurationWindow).UnixNano(), param.StartTimeMax.Add(*traceMaxDurationWindow).UnixNano())
|
||||
|
||||
ctxWithCancel, cancel := context.WithCancel(ctx)
|
||||
|
||||
// search for trace spans and write to `rows []*Row`
|
||||
var rowsLock sync.Mutex
|
||||
var rows []*Row
|
||||
var missingTimeColumn atomic.Bool
|
||||
writeBlock := func(_ uint, db *logstorage.DataBlock) {
|
||||
if missingTimeColumn.Load() {
|
||||
return
|
||||
}
|
||||
|
||||
columns := db.Columns
|
||||
clonedColumnNames := make([]string, len(columns))
|
||||
for i, c := range columns {
|
||||
clonedColumnNames[i] = strings.Clone(c.Name)
|
||||
}
|
||||
|
||||
timestamps, ok := db.GetTimestamps(nil)
|
||||
if !ok {
|
||||
missingTimeColumn.Store(true)
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
|
||||
for i, timestamp := range timestamps {
|
||||
fields := make([]logstorage.Field, 0, len(columns))
|
||||
for j := range columns {
|
||||
// column could be empty if this span does not contain such field.
|
||||
// only append non-empty columns.
|
||||
if columns[j].Values[i] != "" {
|
||||
fields = append(fields, logstorage.Field{Name: clonedColumnNames[j], Value: strings.Clone(columns[j].Values[i])})
|
||||
}
|
||||
}
|
||||
|
||||
rowsLock.Lock()
|
||||
rows = append(rows, &Row{
|
||||
Timestamp: timestamp,
|
||||
Fields: fields,
|
||||
})
|
||||
rowsLock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
if err = vlstorage.RunQuery(ctxWithCancel, cp.TenantIDs, q, writeBlock); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if missingTimeColumn.Load() {
|
||||
return nil, nil, fmt.Errorf("missing _time column in the result for the query [%s]", q)
|
||||
}
|
||||
return traceIDs, rows, nil
|
||||
}
|
||||
|
||||
// getTraceIDList returns traceIDs according to the search params.
|
||||
// It also returns the earliest start time of these traces, to help reducing the time range for spans search.
|
||||
func getTraceIDList(ctx context.Context, cp *CommonParams, param *TraceQueryParam) ([]string, time.Time, error) {
|
||||
currentTime := time.Now()
|
||||
// query: * AND <filter> | last 1 by (_time) partition by (trace_id) | fields _time, trace_id | sort by (_time) desc
|
||||
qStr := "*"
|
||||
if param.ServiceName != "" {
|
||||
qStr += fmt.Sprintf("AND _stream:{"+otelpb.ResourceAttrServiceName+"=%q} ", param.ServiceName)
|
||||
}
|
||||
if param.SpanName != "" {
|
||||
qStr += fmt.Sprintf("AND _stream:{"+otelpb.NameField+"=%q} ", param.SpanName)
|
||||
}
|
||||
if len(param.Attributes) > 0 {
|
||||
for k, v := range param.Attributes {
|
||||
qStr += fmt.Sprintf(`AND %q:=%q `, k, v)
|
||||
}
|
||||
}
|
||||
if param.DurationMin > 0 {
|
||||
qStr += fmt.Sprintf("AND "+otelpb.DurationField+":>%d ", param.DurationMin.Nanoseconds())
|
||||
}
|
||||
if param.DurationMax > 0 {
|
||||
qStr += fmt.Sprintf("AND duration:<%d ", param.DurationMax.Nanoseconds())
|
||||
}
|
||||
qStr += " | last 1 by (_time) partition by (" + otelpb.TraceIDField + ") | fields _time, " + otelpb.TraceIDField + " | sort by (_time) desc"
|
||||
|
||||
q, err := logstorage.ParseQueryAtTimestamp(qStr, currentTime.UnixNano())
|
||||
if err != nil {
|
||||
return nil, time.Time{}, fmt.Errorf("cannot parse query [%s]: %s", qStr, err)
|
||||
}
|
||||
q.AddPipeLimit(uint64(param.Limit))
|
||||
|
||||
traceIDs, maxStartTime, err := findTraceIDsSplitTimeRange(ctx, q, cp, param.StartTimeMin, param.StartTimeMax, param.Limit)
|
||||
if err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
|
||||
return traceIDs, maxStartTime, nil
|
||||
}
|
||||
|
||||
// findTraceIDsSplitTimeRange try to search from the nearest time range of the end time.
|
||||
// if the result already met requirement of `limit`, return.
|
||||
// otherwise, amplify the time range to 5x and search again, until the start time exceed the input.
|
||||
func findTraceIDsSplitTimeRange(ctx context.Context, q *logstorage.Query, cp *CommonParams, startTime, endTime time.Time, limit int) ([]string, time.Time, error) {
|
||||
currentTime := time.Now()
|
||||
|
||||
step := time.Minute
|
||||
currentStartTime := endTime.Add(-step)
|
||||
|
||||
var traceIDListLock sync.Mutex
|
||||
traceIDList := make([]string, 0, limit)
|
||||
maxStartTimeStr := endTime.Format(time.RFC3339)
|
||||
|
||||
writeBlock := func(_ uint, db *logstorage.DataBlock) {
|
||||
columns := db.Columns
|
||||
clonedColumnNames := make([]string, len(columns))
|
||||
for i, c := range columns {
|
||||
clonedColumnNames[i] = strings.Clone(c.Name)
|
||||
}
|
||||
for i := range clonedColumnNames {
|
||||
if clonedColumnNames[i] == "trace_id" {
|
||||
traceIDListLock.Lock()
|
||||
for _, v := range columns[i].Values {
|
||||
traceIDList = append(traceIDList, strings.Clone(v))
|
||||
}
|
||||
traceIDListLock.Unlock()
|
||||
} else if clonedColumnNames[i] == "_time" {
|
||||
for _, v := range columns[i].Values {
|
||||
if v < maxStartTimeStr {
|
||||
maxStartTimeStr = strings.Clone(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for currentStartTime.After(startTime) {
|
||||
qClone := q.CloneWithTimeFilter(currentTime.UnixNano(), currentStartTime.UnixNano(), endTime.UnixNano())
|
||||
if err := vlstorage.RunQuery(ctx, cp.TenantIDs, qClone, writeBlock); err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
|
||||
// found enough trace_id, return directly
|
||||
if len(traceIDList) == limit {
|
||||
maxStartTime, err := time.Parse(time.RFC3339, maxStartTimeStr)
|
||||
if err != nil {
|
||||
return nil, maxStartTime, err
|
||||
}
|
||||
return traceIDList, maxStartTime, nil
|
||||
}
|
||||
|
||||
// not enough trace_id, clear the result, extend the time range and try again.
|
||||
traceIDList = traceIDList[:0]
|
||||
step *= 5
|
||||
currentStartTime = currentStartTime.Add(-step)
|
||||
}
|
||||
|
||||
// one last try with input time range
|
||||
if currentStartTime.Before(startTime) {
|
||||
currentStartTime = startTime
|
||||
}
|
||||
|
||||
qClone := q.CloneWithTimeFilter(currentTime.UnixNano(), currentStartTime.UnixNano(), endTime.UnixNano())
|
||||
if err := vlstorage.RunQuery(ctx, cp.TenantIDs, qClone, writeBlock); err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
|
||||
maxStartTime, err := time.Parse(time.RFC3339, maxStartTimeStr)
|
||||
if err != nil {
|
||||
return nil, maxStartTime, err
|
||||
}
|
||||
|
||||
return checkTraceIDList(traceIDList), maxStartTime, nil
|
||||
}
|
||||
|
||||
// checkTraceIDList removes invalid `trace_id`. It helps prevent query injection.
|
||||
func checkTraceIDList(traceIDList []string) []string {
|
||||
result := make([]string, 0, len(traceIDList))
|
||||
for i := range traceIDList {
|
||||
if traceIDRegex.MatchString(traceIDList[i]) {
|
||||
result = append(result, traceIDList[i])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
23
app/vlselect/traces/query/query_test.go
Normal file
23
app/vlselect/traces/query/query_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCheckTraceIDList(t *testing.T) {
|
||||
f := func(traceID string, valid bool) {
|
||||
t.Helper()
|
||||
|
||||
result := checkTraceIDList([]string{traceID})
|
||||
if valid != (len(result) == 1) {
|
||||
t.Fatalf("check trace id unexpected result, trace_id: %s, valid: %t", traceID, len(result) == 1)
|
||||
}
|
||||
}
|
||||
f("12345678", true)
|
||||
f("abcd1234567", true)
|
||||
f("asdf-asdf-1234-asdf", true)
|
||||
f("abcd1234:4321bcda:4321bacd", true)
|
||||
f("abcd.abcd.1234.4321", true)
|
||||
f("abcd bcad", false)
|
||||
f("abcd\"", false)
|
||||
}
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlstorage/netinsert"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlstorage/netselect"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
@@ -18,6 +16,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -114,9 +113,9 @@ func initLocalStorage() {
|
||||
|
||||
var ss logstorage.StorageStats
|
||||
localStorage.UpdateStats(&ss)
|
||||
logger.Infof("successfully opened storage in %.3f seconds; smallParts: %d; bigParts: %d; smallPartBlocks: %d; bigPartBlocks: %d; smallPartRows: %d; bigPartRows: %d; "+
|
||||
logger.Infof("successfully opened storage %q in %.3f seconds; smallParts: %d; bigParts: %d; smallPartBlocks: %d; bigPartBlocks: %d; smallPartRows: %d; bigPartRows: %d; "+
|
||||
"smallPartSize: %d bytes; bigPartSize: %d bytes",
|
||||
time.Since(startTime).Seconds(), ss.SmallParts, ss.BigParts, ss.SmallPartBlocks, ss.BigPartBlocks, ss.SmallPartRowsCount, ss.BigPartRowsCount,
|
||||
*storageDataPath, time.Since(startTime).Seconds(), ss.SmallParts, ss.BigParts, ss.SmallPartBlocks, ss.BigPartBlocks, ss.SmallPartRowsCount, ss.BigPartRowsCount,
|
||||
ss.CompressedSmallPartSize, ss.CompressedBigPartSize)
|
||||
|
||||
// register local storage metrics
|
||||
|
||||
@@ -454,3 +454,27 @@ func (tc *TestCase) MustStartVlagent(instance string, remoteWriteURLs []string,
|
||||
tc.addApp(instance, app)
|
||||
return app
|
||||
}
|
||||
|
||||
// MustStartDefaultVtsingle is a test helper function that starts an instance of
|
||||
// vtsingle with defaults suitable for most tests.
|
||||
func (tc *TestCase) MustStartDefaultVtsingle() *Vtsingle {
|
||||
tc.t.Helper()
|
||||
|
||||
return tc.MustStartVtsingle("vtsingle", []string{
|
||||
"-storageDataPath=" + tc.Dir() + "/vtsingle",
|
||||
"-retentionPeriod=100y",
|
||||
})
|
||||
}
|
||||
|
||||
// MustStartVtsingle is a test helper function that starts an instance of
|
||||
// vtsingle and fails the test if the app fails to start.
|
||||
func (tc *TestCase) MustStartVtsingle(instance string, flags []string) *Vtsingle {
|
||||
tc.t.Helper()
|
||||
|
||||
app, err := StartVtsingle(instance, flags, tc.cli)
|
||||
if err != nil {
|
||||
tc.t.Fatalf("Could not start %s: %v", instance, err)
|
||||
}
|
||||
tc.addApp(instance, app)
|
||||
return app
|
||||
}
|
||||
|
||||
224
apptest/tests/otlp_ingestion_test.go
Normal file
224
apptest/tests/otlp_ingestion_test.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlselect/traces/query"
|
||||
at "github.com/VictoriaMetrics/VictoriaMetrics/apptest"
|
||||
otelpb "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/pb"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
)
|
||||
|
||||
// TestSingleOTLPIngestionJaegerQuery test data ingestion of `/insert/opentelemetry/v1/traces` API
|
||||
// and queries of various `/select/jaeger/api/*` APIs for vl-single.
|
||||
func TestSingleOTLPIngestionJaegerQuery(t *testing.T) {
|
||||
os.RemoveAll(t.Name())
|
||||
|
||||
tc := at.NewTestCase(t)
|
||||
defer tc.Stop()
|
||||
|
||||
sut := tc.MustStartDefaultVtsingle()
|
||||
|
||||
testOTLPIngestionJaegerQuery(tc, sut)
|
||||
}
|
||||
|
||||
func testOTLPIngestionJaegerQuery(tc *at.TestCase, sut at.VictoriaTracesWriteQuerier) {
|
||||
t := tc.T()
|
||||
|
||||
// prepare test data for ingestion and assertion.
|
||||
serviceName := "testKeyIngestQueryService"
|
||||
spanName := "testKeyIngestQuerySpan"
|
||||
traceID := "123456789"
|
||||
spanID := "987654321"
|
||||
testTagValue := "testValue"
|
||||
testTag := []*otelpb.KeyValue{
|
||||
{
|
||||
Key: "testTag",
|
||||
Value: &otelpb.AnyValue{
|
||||
StringValue: &testTagValue,
|
||||
},
|
||||
},
|
||||
}
|
||||
assertTag := []at.Tag{
|
||||
{
|
||||
Key: "testTag",
|
||||
Type: "string",
|
||||
Value: "testValue",
|
||||
},
|
||||
}
|
||||
spanTime := time.Now()
|
||||
|
||||
req := &otelpb.ExportTraceServiceRequest{
|
||||
ResourceSpans: []*otelpb.ResourceSpans{
|
||||
{
|
||||
Resource: otelpb.Resource{
|
||||
Attributes: []*otelpb.KeyValue{
|
||||
{
|
||||
Key: "service.name",
|
||||
Value: &otelpb.AnyValue{
|
||||
StringValue: &serviceName,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ScopeSpans: []*otelpb.ScopeSpans{
|
||||
{
|
||||
Scope: otelpb.InstrumentationScope{
|
||||
Name: "testInstrumentation",
|
||||
Version: "1.0",
|
||||
Attributes: testTag,
|
||||
DroppedAttributesCount: 999,
|
||||
},
|
||||
Spans: []*otelpb.Span{
|
||||
{
|
||||
TraceID: traceID,
|
||||
SpanID: spanID,
|
||||
TraceState: "trace_state",
|
||||
ParentSpanID: spanID,
|
||||
Flags: 1,
|
||||
Name: spanName,
|
||||
Kind: otelpb.SpanKind(1),
|
||||
StartTimeUnixNano: uint64(spanTime.UnixNano()),
|
||||
EndTimeUnixNano: uint64(spanTime.UnixNano()),
|
||||
Attributes: testTag,
|
||||
Events: []*otelpb.SpanEvent{
|
||||
{
|
||||
TimeUnixNano: uint64(spanTime.UnixNano()),
|
||||
Name: "test event",
|
||||
Attributes: testTag,
|
||||
},
|
||||
},
|
||||
Links: []*otelpb.SpanLink{
|
||||
{
|
||||
TraceID: traceID,
|
||||
SpanID: spanID,
|
||||
TraceState: "trace_state",
|
||||
Attributes: testTag,
|
||||
Flags: 1,
|
||||
},
|
||||
},
|
||||
Status: otelpb.Status{
|
||||
Message: "success",
|
||||
Code: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ingest data via /insert/opentelemetry/v1/traces
|
||||
sut.OTLPExportTraces(t, req, at.QueryOpts{})
|
||||
sut.ForceFlush(t)
|
||||
|
||||
// check services via /select/jaeger/api/services
|
||||
tc.Assert(&at.AssertOptions{
|
||||
Msg: "unexpected /select/jaeger/api/services response",
|
||||
Got: func() any {
|
||||
return sut.JaegerAPIServices(t, at.QueryOpts{})
|
||||
},
|
||||
Want: &at.JaegerAPIServicesResponse{
|
||||
Data: []string{serviceName},
|
||||
},
|
||||
CmpOpts: []cmp.Option{
|
||||
cmpopts.IgnoreFields(at.JaegerAPIServicesResponse{}, "Errors", "Limit", "Offset", "Total"),
|
||||
},
|
||||
})
|
||||
|
||||
// check span name via /select/jaeger/api/services/*/operations
|
||||
tc.Assert(&at.AssertOptions{
|
||||
Msg: "unexpected /select/jaeger/api/services/*/operations response",
|
||||
Got: func() any {
|
||||
return sut.JaegerAPIOperations(t, serviceName, at.QueryOpts{})
|
||||
},
|
||||
Want: &at.JaegerAPIOperationsResponse{
|
||||
Data: []string{spanName},
|
||||
},
|
||||
CmpOpts: []cmp.Option{
|
||||
cmpopts.IgnoreFields(at.JaegerAPIOperationsResponse{}, "Errors", "Limit", "Offset", "Total"),
|
||||
},
|
||||
})
|
||||
|
||||
expectTraceData := []at.TracesResponseData{
|
||||
{
|
||||
Processes: map[string]at.Process{"p1": {ServiceName: "testKeyIngestQueryService", Tags: []at.Tag{}}},
|
||||
Spans: []at.Span{
|
||||
{
|
||||
Duration: 0,
|
||||
TraceID: hex.EncodeToString([]byte(traceID)),
|
||||
SpanID: hex.EncodeToString([]byte(spanID)),
|
||||
Logs: []at.Log{
|
||||
{
|
||||
Timestamp: spanTime.UnixMicro(),
|
||||
Fields: append(assertTag, at.Tag{
|
||||
Key: "event",
|
||||
Type: "string",
|
||||
Value: "test event",
|
||||
}),
|
||||
},
|
||||
},
|
||||
OperationName: spanName,
|
||||
ProcessID: "p1",
|
||||
References: []at.Reference{
|
||||
{
|
||||
TraceID: hex.EncodeToString([]byte(traceID)),
|
||||
SpanID: hex.EncodeToString([]byte(spanID)),
|
||||
RefType: "FOLLOWS_FROM",
|
||||
},
|
||||
},
|
||||
StartTime: spanTime.UnixMicro(),
|
||||
Tags: []at.Tag{
|
||||
{Key: "span.kind", Type: "string", Value: "internal"},
|
||||
{Key: "scope_attr:testTag", Type: "string", Value: "testValue"},
|
||||
{Key: "otel.scope.name", Type: "string", Value: "testInstrumentation"},
|
||||
{Key: "otel.scope.version", Type: "string", Value: "1.0"},
|
||||
{Key: "testTag", Type: "string", Value: "testValue"},
|
||||
{Key: "error", Type: "string", Value: "unset"},
|
||||
{Key: "otel.status_description", Type: "string", Value: "success"},
|
||||
{Key: "w3c.tracestate", Type: "string", Value: "trace_state"},
|
||||
},
|
||||
},
|
||||
},
|
||||
TraceID: hex.EncodeToString([]byte(traceID)),
|
||||
},
|
||||
}
|
||||
|
||||
// check traces data via /select/jaeger/api/traces
|
||||
tc.Assert(&at.AssertOptions{
|
||||
Msg: "unexpected /select/jaeger/api/traces response",
|
||||
Got: func() any {
|
||||
return sut.JaegerAPITraces(t, at.JaegerQueryParam{
|
||||
TraceQueryParam: query.TraceQueryParam{
|
||||
ServiceName: serviceName,
|
||||
StartTimeMin: spanTime.Add(-10 * time.Minute),
|
||||
StartTimeMax: spanTime.Add(10 * time.Minute),
|
||||
},
|
||||
}, at.QueryOpts{})
|
||||
},
|
||||
Want: &at.JaegerAPITracesResponse{
|
||||
Data: expectTraceData,
|
||||
},
|
||||
CmpOpts: []cmp.Option{
|
||||
cmpopts.IgnoreFields(at.JaegerAPITracesResponse{}, "Errors", "Limit", "Offset", "Total"),
|
||||
},
|
||||
})
|
||||
// check single trace data via /select/jaeger/api/traces/<trace_id>
|
||||
tc.Assert(&at.AssertOptions{
|
||||
Msg: "unexpected /select/jaeger/api/traces/<trace_id> response",
|
||||
Got: func() any {
|
||||
return sut.JaegerAPITrace(t, hex.EncodeToString([]byte(traceID)), at.QueryOpts{})
|
||||
},
|
||||
Want: &at.JaegerAPITraceResponse{
|
||||
Data: expectTraceData,
|
||||
},
|
||||
CmpOpts: []cmp.Option{
|
||||
cmpopts.IgnoreFields(at.JaegerAPITraceResponse{}, "Errors", "Limit", "Offset", "Total"),
|
||||
},
|
||||
})
|
||||
}
|
||||
211
apptest/traces_model.go
Normal file
211
apptest/traces_model.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package apptest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlselect/traces/query"
|
||||
otelpb "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/pb"
|
||||
)
|
||||
|
||||
// VictoriaTracesWriteQuerier encompasses the methods for writing, flushing and
|
||||
// querying the trace data.
|
||||
type VictoriaTracesWriteQuerier interface {
|
||||
OTLPTracesWriter
|
||||
JaegerQuerier
|
||||
StorageFlusher
|
||||
StorageMerger
|
||||
}
|
||||
|
||||
// JaegerQuerier contains methods available to Jaeger HTTP API for Querying.
|
||||
type JaegerQuerier interface {
|
||||
JaegerAPIServices(t *testing.T, opts QueryOpts) *JaegerAPIServicesResponse
|
||||
JaegerAPIOperations(t *testing.T, serviceName string, opts QueryOpts) *JaegerAPIOperationsResponse
|
||||
JaegerAPITraces(t *testing.T, params JaegerQueryParam, opts QueryOpts) *JaegerAPITracesResponse
|
||||
JaegerAPITrace(t *testing.T, traceID string, opts QueryOpts) *JaegerAPITraceResponse
|
||||
JaegerAPIDependencies(t *testing.T, opts QueryOpts)
|
||||
}
|
||||
|
||||
// OTLPTracesWriter contains methods for writing OTLP trace data.
|
||||
type OTLPTracesWriter interface {
|
||||
OTLPExportTraces(t *testing.T, request *otelpb.ExportTraceServiceRequest, opts QueryOpts)
|
||||
}
|
||||
|
||||
// JaegerQueryParam is a helper structure for implementing extra
|
||||
// helper functions of `query.TraceQueryParam`.
|
||||
type JaegerQueryParam struct {
|
||||
query.TraceQueryParam
|
||||
}
|
||||
|
||||
// asURLValues add non-empty jaeger query params as URL values.
|
||||
func (jqp *JaegerQueryParam) asURLValues() url.Values {
|
||||
uv := make(url.Values)
|
||||
addNonEmpty := func(name string, values ...string) {
|
||||
for _, value := range values {
|
||||
if len(value) == 0 {
|
||||
continue
|
||||
}
|
||||
uv.Add(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
addNonEmpty("service", jqp.ServiceName)
|
||||
addNonEmpty("operation", jqp.SpanName)
|
||||
|
||||
if len(jqp.Attributes) > 0 {
|
||||
b, _ := json.Marshal(jqp.Attributes)
|
||||
uv.Add("tags", string(b))
|
||||
}
|
||||
if jqp.DurationMin > 0 {
|
||||
uv.Add("minDuration", strconv.FormatInt(jqp.DurationMin.Milliseconds(), 10)+"ms")
|
||||
}
|
||||
if jqp.DurationMax > 0 {
|
||||
uv.Add("maxDuration", strconv.FormatInt(jqp.DurationMax.Milliseconds(), 10)+"ms")
|
||||
}
|
||||
if jqp.Limit > 0 {
|
||||
uv.Add("limit", strconv.Itoa(jqp.Limit))
|
||||
}
|
||||
if !jqp.StartTimeMin.IsZero() {
|
||||
uv.Add("start", strconv.FormatInt(jqp.StartTimeMin.UnixMicro(), 10))
|
||||
}
|
||||
if !jqp.StartTimeMax.IsZero() {
|
||||
uv.Add("end", strconv.FormatInt(jqp.StartTimeMax.UnixMicro(), 10))
|
||||
}
|
||||
|
||||
return uv
|
||||
}
|
||||
|
||||
// JaegerResponse contains the common fields shared by all responses of Jaeger query APIs.
|
||||
type JaegerResponse struct {
|
||||
Errors interface{} `json:"errors"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// JaegerAPIServicesResponse is an in-memory representation of the
|
||||
// /select/jaeger/services response.
|
||||
type JaegerAPIServicesResponse struct {
|
||||
Data []string `json:"data"`
|
||||
JaegerResponse
|
||||
}
|
||||
|
||||
// JaegerAPIOperationsResponse is an in-memory representation of the
|
||||
// /select/jaeger/services/<service_name>/operations response.
|
||||
type JaegerAPIOperationsResponse struct {
|
||||
Data []string `json:"data"`
|
||||
JaegerResponse
|
||||
}
|
||||
|
||||
// JaegerAPITracesResponse is an in-memory representation of the
|
||||
// /select/jaeger/traces response.
|
||||
type JaegerAPITracesResponse struct {
|
||||
Data []TracesResponseData `json:"data"`
|
||||
JaegerResponse
|
||||
}
|
||||
|
||||
// JaegerAPITraceResponse is an in-memory representation of the
|
||||
// /select/jaeger/traces/<trace_id> response.
|
||||
type JaegerAPITraceResponse struct {
|
||||
Data []TracesResponseData `json:"data"`
|
||||
JaegerResponse
|
||||
}
|
||||
|
||||
// TracesResponseData is the structure of `data` field of the
|
||||
// /select/jaeger/traces and /select/jaeger/traces/<trace_id> response.
|
||||
type TracesResponseData struct {
|
||||
Processes map[string]Process `json:"processes"`
|
||||
Spans []Span `json:"spans"`
|
||||
TraceID string `json:"traceID"`
|
||||
Warnings interface{} `json:"warnings"`
|
||||
}
|
||||
|
||||
// Process is the structure for Jaeger Process.
|
||||
type Process struct {
|
||||
ServiceName string `json:"serviceName"`
|
||||
Tags []Tag `json:"tags"`
|
||||
}
|
||||
|
||||
// Tag is the structure for Jaeger tag.
|
||||
type Tag struct {
|
||||
Key string `json:"key"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// Span is the structure for Jaeger Span.
|
||||
type Span struct {
|
||||
Duration int `json:"duration"`
|
||||
Logs []Log `json:"logs"`
|
||||
OperationName string `json:"operationName"`
|
||||
ProcessID string `json:"processID"`
|
||||
References []Reference `json:"references"`
|
||||
SpanID string `json:"spanID"`
|
||||
StartTime int64 `json:"startTime"`
|
||||
Tags []Tag `json:"tags"`
|
||||
TraceID string `json:"traceID"`
|
||||
Warnings interface{} `json:"warnings"`
|
||||
}
|
||||
|
||||
// Log is the structure for Jaeger Log.
|
||||
type Log struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Fields []Tag `json:"fields"`
|
||||
}
|
||||
|
||||
// Reference is the structure for Jaeger Reference.
|
||||
type Reference struct {
|
||||
RefType string `json:"refType"`
|
||||
SpanID string `json:"spanID"`
|
||||
TraceID string `json:"traceID"`
|
||||
}
|
||||
|
||||
// NewJaegerAPIServicesResponse is a test helper function that creates a new
|
||||
// instance of JaegerAPIServicesResponse by unmarshalling a json string.
|
||||
func NewJaegerAPIServicesResponse(t *testing.T, s string) *JaegerAPIServicesResponse {
|
||||
t.Helper()
|
||||
|
||||
res := &JaegerAPIServicesResponse{}
|
||||
if err := json.Unmarshal([]byte(s), res); err != nil {
|
||||
t.Fatalf("could not unmarshal query response data=\n%s\n: %v", string(s), err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// NewJaegerAPIOperationsResponse is a test helper function that creates a new
|
||||
// instance of JaegerAPIOperationsResponse by unmarshalling a json string.
|
||||
func NewJaegerAPIOperationsResponse(t *testing.T, s string) *JaegerAPIOperationsResponse {
|
||||
t.Helper()
|
||||
|
||||
res := &JaegerAPIOperationsResponse{}
|
||||
if err := json.Unmarshal([]byte(s), res); err != nil {
|
||||
t.Fatalf("could not unmarshal query response data=\n%s\n: %v", string(s), err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// NewJaegerAPITracesResponse is a test helper function that creates a new
|
||||
// instance of JaegerAPITracesResponse by unmarshalling a json string.
|
||||
func NewJaegerAPITracesResponse(t *testing.T, s string) *JaegerAPITracesResponse {
|
||||
t.Helper()
|
||||
|
||||
res := &JaegerAPITracesResponse{}
|
||||
if err := json.Unmarshal([]byte(s), res); err != nil {
|
||||
t.Fatalf("could not unmarshal query response data=\n%s\n: %v", string(s), err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// NewJaegerAPITraceResponse is a test helper function that creates a new
|
||||
// instance of JaegerAPITraceResponse by unmarshalling a json string.
|
||||
func NewJaegerAPITraceResponse(t *testing.T, s string) *JaegerAPITraceResponse {
|
||||
t.Helper()
|
||||
|
||||
res := &JaegerAPITraceResponse{}
|
||||
if err := json.Unmarshal([]byte(s), res); err != nil {
|
||||
t.Fatalf("could not unmarshal query response data=\n%s\n: %v", string(s), err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
171
apptest/vtsingle.go
Normal file
171
apptest/vtsingle.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package apptest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
otelpb "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/pb"
|
||||
)
|
||||
|
||||
// Vtsingle holds the state of a Vtsingle app and provides Vtsingle-specific
|
||||
// functions.
|
||||
type Vtsingle struct {
|
||||
*app
|
||||
*ServesMetrics
|
||||
|
||||
storageDataPath string
|
||||
httpListenAddr string
|
||||
|
||||
forceFlushURL string
|
||||
forceMergeURL string
|
||||
|
||||
jaegerAPIServicesURL string
|
||||
jaegerAPIOperationsURL string
|
||||
jaegerAPITracesURL string
|
||||
jaegerAPITraceURL string
|
||||
|
||||
otlpTracesURL string
|
||||
}
|
||||
|
||||
// StartVtsingle starts an instance of Vtsingle with the given flags. It also
|
||||
// sets the default flags and populates the app instance state with runtime
|
||||
// values extracted from the application log (such as httpListenAddr).
|
||||
func StartVtsingle(instance string, flags []string, cli *Client) (*Vtsingle, error) {
|
||||
app, stderrExtracts, err := startApp(instance, "../../bin/victoria-logs", flags, &appOptions{
|
||||
defaultFlags: map[string]string{
|
||||
"-storageDataPath": fmt.Sprintf("%s/%s-%d", os.TempDir(), instance, time.Now().UnixNano()),
|
||||
"-httpListenAddr": "127.0.0.1:0",
|
||||
},
|
||||
extractREs: []*regexp.Regexp{
|
||||
logsStorageDataPathRE,
|
||||
httpListenAddrRE,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Vtsingle{
|
||||
app: app,
|
||||
ServesMetrics: &ServesMetrics{
|
||||
metricsURL: fmt.Sprintf("http://%s/metrics", stderrExtracts[1]),
|
||||
cli: cli,
|
||||
},
|
||||
storageDataPath: stderrExtracts[0],
|
||||
httpListenAddr: stderrExtracts[1],
|
||||
|
||||
forceFlushURL: fmt.Sprintf("http://%s/internal/force_flush", stderrExtracts[1]),
|
||||
forceMergeURL: fmt.Sprintf("http://%s/internal/force_merge", stderrExtracts[1]),
|
||||
|
||||
jaegerAPIServicesURL: fmt.Sprintf("http://%s/select/jaeger/api/services", stderrExtracts[1]),
|
||||
jaegerAPIOperationsURL: fmt.Sprintf("http://%s/select/jaeger/api/services/%%s/operations", stderrExtracts[1]),
|
||||
jaegerAPITracesURL: fmt.Sprintf("http://%s/select/jaeger/api/traces", stderrExtracts[1]),
|
||||
jaegerAPITraceURL: fmt.Sprintf("http://%s/select/jaeger/api/traces/%%s", stderrExtracts[1]),
|
||||
|
||||
otlpTracesURL: fmt.Sprintf("http://%s/insert/opentelemetry/v1/traces", stderrExtracts[1]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ForceFlush is a test helper function that forces the flushing of inserted
|
||||
// data, so it becomes available for searching immediately.
|
||||
func (app *Vtsingle) ForceFlush(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
_, statusCode := app.cli.Get(t, app.forceFlushURL)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// ForceMerge is a test helper function that forces the merging of parts.
|
||||
func (app *Vtsingle) ForceMerge(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
_, statusCode := app.cli.Get(t, app.forceMergeURL)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// JaegerAPIServices is a test helper function that queries for service list
|
||||
// by sending an HTTP GET request to /select/jaeger/api/services
|
||||
// Vtsingle endpoint.
|
||||
func (app *Vtsingle) JaegerAPIServices(t *testing.T, opts QueryOpts) *JaegerAPIServicesResponse {
|
||||
t.Helper()
|
||||
|
||||
res, _ := app.cli.Get(t, app.jaegerAPIServicesURL+"?"+opts.asURLValues().Encode())
|
||||
return NewJaegerAPIServicesResponse(t, res)
|
||||
}
|
||||
|
||||
// JaegerAPIOperations is a test helper function that queries for operation list of a service
|
||||
// by sending an HTTP GET request to /select/jaeger/api/services/<service_name>/operations
|
||||
// Vtsingle endpoint.
|
||||
func (app *Vtsingle) JaegerAPIOperations(t *testing.T, serviceName string, opts QueryOpts) *JaegerAPIOperationsResponse {
|
||||
t.Helper()
|
||||
|
||||
url := fmt.Sprintf(app.jaegerAPIOperationsURL, serviceName) + "?" + opts.asURLValues().Encode()
|
||||
res, _ := app.cli.Get(t, url)
|
||||
return NewJaegerAPIOperationsResponse(t, res)
|
||||
}
|
||||
|
||||
// JaegerAPITraces is a test helper function that queries for traces with filter conditions
|
||||
// by sending an HTTP GET request to /select/jaeger/api/traces Vtsingle endpoint.
|
||||
func (app *Vtsingle) JaegerAPITraces(t *testing.T, param JaegerQueryParam, opts QueryOpts) *JaegerAPITracesResponse {
|
||||
t.Helper()
|
||||
|
||||
paramsEnc := "?"
|
||||
values := opts.asURLValues()
|
||||
if len(values) > 0 {
|
||||
paramsEnc += values.Encode() + "&"
|
||||
}
|
||||
uv := param.asURLValues()
|
||||
if len(uv) > 0 {
|
||||
paramsEnc += uv.Encode()
|
||||
}
|
||||
res, _ := app.cli.Get(t, app.jaegerAPITracesURL+paramsEnc)
|
||||
return NewJaegerAPITracesResponse(t, res)
|
||||
}
|
||||
|
||||
// JaegerAPITrace is a test helper function that queries for a single trace with trace_id
|
||||
// by sending an HTTP GET request to /select/jaeger/api/traces/<trace_id>
|
||||
// Vtsingle endpoint.
|
||||
func (app *Vtsingle) JaegerAPITrace(t *testing.T, traceID string, opts QueryOpts) *JaegerAPITraceResponse {
|
||||
t.Helper()
|
||||
|
||||
url := fmt.Sprintf(app.jaegerAPITraceURL, traceID)
|
||||
res, _ := app.cli.Get(t, url+"?"+opts.asURLValues().Encode())
|
||||
return NewJaegerAPITraceResponse(t, res)
|
||||
}
|
||||
|
||||
// JaegerAPIDependencies is a test helper function that queries for the dependencies.
|
||||
// This method is not implemented in Vtsingle and this test is no-op for now.
|
||||
func (app *Vtsingle) JaegerAPIDependencies(_ *testing.T, _ QueryOpts) {}
|
||||
|
||||
// OTLPExportTraces is a test helper function that exports OTLP trace data
|
||||
// by sending an HTTP POST request to /insert/opentelemetry/v1/traces
|
||||
// Vtsingle endpoint.
|
||||
func (app *Vtsingle) OTLPExportTraces(t *testing.T, request *otelpb.ExportTraceServiceRequest, _ QueryOpts) {
|
||||
t.Helper()
|
||||
|
||||
pbData := request.MarshalProtobuf(nil)
|
||||
body, code := app.cli.Post(t, app.otlpTracesURL, "application/x-protobuf", pbData)
|
||||
if code != 200 {
|
||||
t.Fatalf("got %d, expected 200. body: %s", code, body)
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPAddr returns the address at which the vtstorage process is listening
|
||||
// for http connections.
|
||||
func (app *Vtsingle) HTTPAddr() string {
|
||||
return app.httpListenAddr
|
||||
}
|
||||
|
||||
// String returns the string representation of the Vtsingle app state.
|
||||
func (app *Vtsingle) String() string {
|
||||
return fmt.Sprintf("{app: %s storageDataPath: %q httpListenAddr: %q}", []any{
|
||||
app.app, app.storageDataPath, app.httpListenAddr}...)
|
||||
}
|
||||
@@ -249,6 +249,13 @@ docker-vl-cluster-up:
|
||||
docker-vl-cluster-down:
|
||||
$(DOCKER_COMPOSE) -f deployment/docker/compose-vl-cluster.yml down -v
|
||||
|
||||
# VT single
|
||||
docker-vt-single-up:
|
||||
$(DOCKER_COMPOSE) -f deployment/docker/compose-vt-single.yml up -d
|
||||
|
||||
docker-vt-single-down:
|
||||
$(DOCKER_COMPOSE) -f deployment/docker/compose-vt-single.yml down -v
|
||||
|
||||
# Command aliases to keep backward-compatibility, as they could have been mentioned on the Internet before the rename.
|
||||
docker-single-up: docker-vm-single-up
|
||||
docker-single-down: docker-vm-single-down
|
||||
|
||||
@@ -15,6 +15,8 @@ to the Internet.
|
||||
* Logs:
|
||||
* [VictoriaLogs single server](#victoriaLogs-server)
|
||||
* [VictoriaLogs cluster](#victoriaLogs-cluster)
|
||||
* Traces:
|
||||
* [VictoriaTraces single server](#victoriaTraces-server)
|
||||
* [Common](#common-components)
|
||||
* [vmauth](#vmauth)
|
||||
* [vmalert](#vmalert)
|
||||
@@ -198,6 +200,40 @@ Please see more examples on integration of VictoriaLogs with other log shippers
|
||||
* [fluentd](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker/victorialogs/fluentd)
|
||||
* [datadog-serverless](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker/victorialogs/datadog-serverless)
|
||||
|
||||
## VictoriaTraces server
|
||||
|
||||
To spin-up environment with [VictoriaTraces](https://docs.victoriametrics.com/victoriatraces/) run the following command:
|
||||
```sh
|
||||
# checkout to `victoriatraces` branch
|
||||
git clone -b victoriatraces --single-branch https://github.com/VictoriaMetrics/VictoriaMetrics.git
|
||||
cd VictoriaMetrics
|
||||
|
||||
# start docker compose
|
||||
make docker-vt-single-up
|
||||
```
|
||||
_See [compose-vt-single.yml](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/victoriatraces/deployment/docker/compose-vt-single.yml)_
|
||||
|
||||
VictoriaTraces will be accessible on the `--httpListenAddr=:9428` port.
|
||||
|
||||
In addition to VictoriaTraces server, the docker compose contains the following components:
|
||||
* [HotROD](https://hub.docker.com/r/jaegertracing/example-hotrod) application to generate trace data.
|
||||
* `VictoriaMetrics single-node` to collect metrics from all the components;
|
||||
* [Grafana](#grafana) is configured with [VictoriaMetrics](https://github.com/VictoriaMetrics/victoriametrics-datasource) and Jaeger datasource pointing to VictoriaTraces server.
|
||||
|
||||
<img alt="VictoriaTraces single-server deployment" width="500" src="assets/vt-single-server.png">
|
||||
|
||||
To generate trace data, you need to access HotROD at [http://localhost:8080](http://localhost:8080), and **click any button on the page**.
|
||||
|
||||
To access Grafana, use link [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
To access [VictoriaTraces UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui),
|
||||
use link [http://localhost:9428/select/vmui](http://localhost:9428/select/vmui).
|
||||
|
||||
To shut down environment execute the following command:
|
||||
```
|
||||
make docker-vt-single-down
|
||||
```
|
||||
|
||||
# Common components
|
||||
|
||||
## vmauth
|
||||
|
||||
BIN
deployment/docker/assets/vt-single-server.png
Normal file
BIN
deployment/docker/assets/vt-single-server.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 181 KiB |
61
deployment/docker/compose-vt-single.yml
Normal file
61
deployment/docker/compose-vt-single.yml
Normal file
@@ -0,0 +1,61 @@
|
||||
services:
|
||||
|
||||
hotrod:
|
||||
# HotROD (Rides on Demand) is a demo application that generates trace data and sends to VictoriaTraces.
|
||||
# Visit :8080 after the deployment and click any button on the page to generate trace spans.
|
||||
image: jaegertracing/example-hotrod:1.70.0
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "8083:8083"
|
||||
command:
|
||||
- "all"
|
||||
environment:
|
||||
- OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://victoriatraces:9428/insert/opentelemetry/v1/traces
|
||||
depends_on:
|
||||
- victoriatraces
|
||||
|
||||
# Grafana instance configured with VictoriaLogs as datasource
|
||||
grafana:
|
||||
image: grafana/grafana:12.0.2
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
- "victoriatraces"
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
- grafanadata:/var/lib/grafana
|
||||
- ./provisioning/datasources/victoriametrics-traces-datasource/single.yml:/etc/grafana/provisioning/datasources/single.yml
|
||||
- ./provisioning/dashboards:/etc/grafana/provisioning/dashboards
|
||||
- ./provisioning/plugins/:/var/lib/grafana/plugins
|
||||
restart: always
|
||||
|
||||
# VictoriaTraces instance, a single process responsible for
|
||||
# storing trace span and serving read queries.
|
||||
victoriatraces:
|
||||
# todo: use official image when it is released
|
||||
image: 'docker.io/victoriametrics/victoria-traces:heads-victoriatraces-0-g4429ac366EXTRA_DOCKER_TAG_SUFFIX'
|
||||
ports:
|
||||
- "9428:9428"
|
||||
command:
|
||||
- "--storageDataPath=/vtraces"
|
||||
volumes:
|
||||
- vtdata:/vtraces
|
||||
|
||||
# VictoriaMetrics instance, a single process responsible for
|
||||
# scraping, storing metrics and serve read requests.
|
||||
victoriametrics:
|
||||
image: victoriametrics/victoria-metrics:v1.120.0
|
||||
ports:
|
||||
- "8428:8428"
|
||||
volumes:
|
||||
- vmdata:/storage
|
||||
- ./prometheus-vt-single.yml:/etc/prometheus/prometheus.yml
|
||||
command:
|
||||
- "--storageDataPath=/storage"
|
||||
- "--promscrape.config=/etc/prometheus/prometheus.yml"
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
vmdata: {}
|
||||
vtdata: {}
|
||||
grafanadata: {}
|
||||
12
deployment/docker/prometheus-vt-single.yml
Normal file
12
deployment/docker/prometheus-vt-single.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
global:
|
||||
scrape_interval: 10s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: victoriametrics
|
||||
static_configs:
|
||||
- targets:
|
||||
- victoriametrics:8428
|
||||
- job_name: victoriatraces
|
||||
static_configs:
|
||||
- targets:
|
||||
- victoriatraces:9428
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: VictoriaTraces
|
||||
type: jaeger
|
||||
access: proxy
|
||||
url: http://victoriatraces:9428/select/jaeger/
|
||||
|
||||
- name: VictoriaMetrics
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://victoriametrics:8428/
|
||||
22
lib/hashpool/hashpool.go
Normal file
22
lib/hashpool/hashpool.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package hashpool
|
||||
|
||||
import (
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var xxhashPool = &sync.Pool{
|
||||
New: func() any {
|
||||
return xxhash.New()
|
||||
},
|
||||
}
|
||||
|
||||
// Get return a *xxhash.Digest from hash pool.
|
||||
func Get() *xxhash.Digest {
|
||||
return xxhashPool.Get().(*xxhash.Digest)
|
||||
}
|
||||
|
||||
// Put a *xxhash.Digest back to the hash pool.
|
||||
func Put(x *xxhash.Digest) {
|
||||
xxhashPool.Put(x)
|
||||
}
|
||||
@@ -13,13 +13,12 @@ import (
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
"github.com/cespare/xxhash/v2"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/hashpool"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/stringsutil"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
var maxDroppedTargets = flag.Int("promscrape.maxDroppedTargets", 10000, "The maximum number of droppedTargets to show at /api/v1/targets page. "+
|
||||
@@ -410,7 +409,7 @@ func (dt *droppedTargets) getTotalTargets() int {
|
||||
}
|
||||
|
||||
func labelsHash(labels *promutil.Labels) uint64 {
|
||||
d := xxhashPool.Get().(*xxhash.Digest)
|
||||
d := hashpool.Get()
|
||||
for _, label := range labels.GetLabels() {
|
||||
// exclude annotations from hash generation
|
||||
// annotations are mutable and should not be used for objects identification
|
||||
@@ -423,16 +422,10 @@ func labelsHash(labels *promutil.Labels) uint64 {
|
||||
}
|
||||
h := d.Sum64()
|
||||
d.Reset()
|
||||
xxhashPool.Put(d)
|
||||
hashpool.Put(d)
|
||||
return h
|
||||
}
|
||||
|
||||
var xxhashPool = &sync.Pool{
|
||||
New: func() any {
|
||||
return xxhash.New()
|
||||
},
|
||||
}
|
||||
|
||||
// WriteDroppedTargetsJSON writes `droppedTargets` contents to w according to https://prometheus.io/docs/prometheus/latest/querying/api/#targets
|
||||
func (dt *droppedTargets) WriteDroppedTargetsJSON(w io.Writer) {
|
||||
dts := dt.getTargetsList()
|
||||
|
||||
68
lib/protoparser/opentelemetry/pb/trace_fields.go
Normal file
68
lib/protoparser/opentelemetry/pb/trace_fields.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package pb
|
||||
|
||||
// trace_fields.go contains field names when storing OTLP trace span data in VictoriaLogs.
|
||||
|
||||
// Resource
|
||||
const (
|
||||
ResourceAttrPrefix = "resource_attr:"
|
||||
ResourceAttrServiceName = "resource_attr:service.name" // ResourceAttrServiceName service name is a special resource attribute
|
||||
)
|
||||
|
||||
// ScopeSpans - InstrumentationScope
|
||||
const (
|
||||
InstrumentationScopeName = "scope_name"
|
||||
InstrumentationScopeVersion = "scope_version"
|
||||
InstrumentationScopeAttrPrefix = "scope_attr:"
|
||||
)
|
||||
|
||||
// Span
|
||||
const (
|
||||
TraceIDField = "trace_id"
|
||||
SpanIDField = "span_id"
|
||||
TraceStateField = "trace_state"
|
||||
ParentSpanIDField = "parent_span_id"
|
||||
FlagsField = "flags"
|
||||
NameField = "name"
|
||||
KindField = "kind"
|
||||
StartTimeUnixNanoField = "start_time_unix_nano"
|
||||
EndTimeUnixNanoField = "end_time_unix_nano"
|
||||
SpanAttrPrefixField = "span_attr:"
|
||||
DroppedAttributesCountField = "dropped_attributes_count"
|
||||
// Span_Event Here
|
||||
DroppedEventsCountField = "dropped_events_count"
|
||||
// Span_Link Here
|
||||
DroppedLinksCountField = "dropped_links_count"
|
||||
// Status Here
|
||||
|
||||
// DurationField field is calculated by end-start to allow duration filter on span.
|
||||
// It's not part of OTLP.
|
||||
DurationField = "duration"
|
||||
)
|
||||
|
||||
// Span_Event
|
||||
const (
|
||||
EventPrefix = "event:"
|
||||
|
||||
EventTimeUnixNanoField = "event_time_unix_nano"
|
||||
EventNameField = "event_name"
|
||||
EventAttrPrefix = "event_attr:"
|
||||
EventDroppedAttributesCountField = "event_dropped_attributes_count"
|
||||
)
|
||||
|
||||
// Span_Link
|
||||
const (
|
||||
LinkPrefix = "link:"
|
||||
|
||||
LinkTraceIDField = "link_trace_id"
|
||||
LinkSpanIDField = "link_span_id"
|
||||
LinkTraceStateField = "link_trace_state"
|
||||
LinkAttrPrefix = "link_attr:"
|
||||
LinkDroppedAttributesCountField = "link_dropped_attributes_count"
|
||||
LinkFlagsField = "link_flags"
|
||||
)
|
||||
|
||||
// Status
|
||||
const (
|
||||
StatusMessageField = "status_message"
|
||||
StatusCodeField = "status_code"
|
||||
)
|
||||
664
lib/protoparser/opentelemetry/pb/traces.go
Normal file
664
lib/protoparser/opentelemetry/pb/traces.go
Normal file
@@ -0,0 +1,664 @@
|
||||
package pb
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/VictoriaMetrics/easyproto"
|
||||
)
|
||||
|
||||
type (
|
||||
// SpanKind see: https://opentelemetry.io/docs/specs/otel/trace/api/#spankind
|
||||
SpanKind int32
|
||||
// StatusCode see: https://opentelemetry.io/docs/specs/otel/trace/api/#set-status
|
||||
StatusCode int32
|
||||
)
|
||||
|
||||
// ExportTraceServiceRequest represent the OTLP protobuf message
|
||||
//
|
||||
// https://github.com/open-telemetry/opentelemetry-proto/blob/v1.5.0/opentelemetry/proto/collector/trace/v1/trace_service.proto#L36
|
||||
// https://github.com/open-telemetry/opentelemetry-collector/blob/v0.124.0/pdata/internal/data/protogen/collector/trace/v1/trace_service.pb.go#L33
|
||||
type ExportTraceServiceRequest struct {
|
||||
ResourceSpans []*ResourceSpans
|
||||
}
|
||||
|
||||
// MarshalProtobuf marshals r to protobuf message, appends it to dst and returns the result.
|
||||
func (r *ExportTraceServiceRequest) MarshalProtobuf(dst []byte) []byte {
|
||||
m := mp.Get()
|
||||
r.marshalProtobuf(m.MessageMarshaler())
|
||||
dst = m.Marshal(dst)
|
||||
mp.Put(m)
|
||||
return dst
|
||||
}
|
||||
|
||||
func (r *ExportTraceServiceRequest) marshalProtobuf(mm *easyproto.MessageMarshaler) {
|
||||
//message ExportTraceServiceRequest {
|
||||
// repeated opentelemetry.proto.trace.v1.ResourceSpans resource_spans = 1;
|
||||
//}
|
||||
for _, rs := range r.ResourceSpans {
|
||||
rs.marshalProtobuf(mm.AppendMessage(1))
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalProtobuf unmarshals r from protobuf message at src.
|
||||
func (r *ExportTraceServiceRequest) UnmarshalProtobuf(src []byte) (err error) {
|
||||
var fc easyproto.FieldContext
|
||||
for len(src) > 0 {
|
||||
src, err = fc.NextField(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot read next field in ExportTraceServiceRequest: %w", err)
|
||||
}
|
||||
switch fc.FieldNum {
|
||||
case 1:
|
||||
data, ok := fc.MessageData()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read resource spans data")
|
||||
}
|
||||
r.ResourceSpans = append(r.ResourceSpans, &ResourceSpans{})
|
||||
a := r.ResourceSpans[len(r.ResourceSpans)-1]
|
||||
if err = a.unmarshalProtobuf(data); err != nil {
|
||||
return fmt.Errorf("cannot unmarshal resource span: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResourceSpans represent a collection of ScopeSpans from a Resource.
|
||||
//
|
||||
// https://github.com/open-telemetry/opentelemetry-proto/blob/v1.5.0/opentelemetry/proto/trace/v1/trace.proto#L48
|
||||
// https://github.com/open-telemetry/opentelemetry-collector/blob/v0.124.0/pdata/internal/data/protogen/trace/v1/trace.pb.go#L230
|
||||
type ResourceSpans struct {
|
||||
Resource Resource
|
||||
ScopeSpans []*ScopeSpans
|
||||
SchemaURL string
|
||||
}
|
||||
|
||||
func (rs *ResourceSpans) marshalProtobuf(mm *easyproto.MessageMarshaler) {
|
||||
//message ResourceSpans {
|
||||
// opentelemetry.proto.resource.v1.Resource resource = 1;
|
||||
// repeated ScopeSpans scope_spans = 2;
|
||||
// string schema_url = 3;
|
||||
//}
|
||||
rs.Resource.marshalProtobuf(mm.AppendMessage(1))
|
||||
for _, ss := range rs.ScopeSpans {
|
||||
ss.marshalProtobuf(mm.AppendMessage(2))
|
||||
}
|
||||
mm.AppendString(3, rs.SchemaURL)
|
||||
}
|
||||
|
||||
func (rs *ResourceSpans) unmarshalProtobuf(src []byte) (err error) {
|
||||
var fc easyproto.FieldContext
|
||||
for len(src) > 0 {
|
||||
src, err = fc.NextField(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot read next field in Status: %w", err)
|
||||
}
|
||||
switch fc.FieldNum {
|
||||
case 1:
|
||||
data, ok := fc.MessageData()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read resource span resource data")
|
||||
}
|
||||
if err = rs.Resource.unmarshalProtobuf(data); err != nil {
|
||||
return fmt.Errorf("cannot unmarshal resource span resource: %w", err)
|
||||
}
|
||||
case 2:
|
||||
data, ok := fc.MessageData()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read resource span scope span data")
|
||||
}
|
||||
rs.ScopeSpans = append(rs.ScopeSpans, &ScopeSpans{})
|
||||
a := rs.ScopeSpans[len(rs.ScopeSpans)-1]
|
||||
if err = a.unmarshalProtobuf(data); err != nil {
|
||||
return fmt.Errorf("cannot unmarshal resource span scope span: %w", err)
|
||||
}
|
||||
case 3:
|
||||
schemaURL, ok := fc.String()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read resource span schema url")
|
||||
}
|
||||
rs.SchemaURL = strings.Clone(schemaURL)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ScopeSpans represent a collection of Spans produced by an InstrumentationScope.
|
||||
//
|
||||
// https://github.com/open-telemetry/opentelemetry-proto/blob/v1.5.0/opentelemetry/proto/trace/v1/trace.proto#L68
|
||||
// https://github.com/open-telemetry/opentelemetry-collector/blob/v0.124.0/pdata/internal/data/protogen/trace/v1/trace.pb.go#L308
|
||||
type ScopeSpans struct {
|
||||
Scope InstrumentationScope
|
||||
Spans []*Span
|
||||
SchemaURL string
|
||||
}
|
||||
|
||||
func (ss *ScopeSpans) marshalProtobuf(mm *easyproto.MessageMarshaler) {
|
||||
//message ScopeSpans {
|
||||
// opentelemetry.proto.common.v1.InstrumentationScope scope = 1;
|
||||
// repeated Span spans = 2;
|
||||
// string schema_url = 3;
|
||||
//}
|
||||
ss.Scope.marshalProtobuf(mm.AppendMessage(1))
|
||||
for _, span := range ss.Spans {
|
||||
span.marshalProtobuf(mm.AppendMessage(2))
|
||||
}
|
||||
mm.AppendString(3, ss.SchemaURL)
|
||||
}
|
||||
|
||||
func (ss *ScopeSpans) unmarshalProtobuf(src []byte) (err error) {
|
||||
var fc easyproto.FieldContext
|
||||
for len(src) > 0 {
|
||||
src, err = fc.NextField(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot read next field in Status: %w", err)
|
||||
}
|
||||
switch fc.FieldNum {
|
||||
case 1:
|
||||
data, ok := fc.MessageData()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read scope span scope data")
|
||||
}
|
||||
if err = ss.Scope.unmarshalProtobuf(data); err != nil {
|
||||
return fmt.Errorf("cannot unmarshal scope span scope: %w", err)
|
||||
}
|
||||
case 2:
|
||||
data, ok := fc.MessageData()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read scope span span data")
|
||||
}
|
||||
ss.Spans = append(ss.Spans, &Span{})
|
||||
a := ss.Spans[len(ss.Spans)-1]
|
||||
if err = a.unmarshalProtobuf(data); err != nil {
|
||||
return fmt.Errorf("cannot unmarshal scope span span: %w", err)
|
||||
}
|
||||
case 3:
|
||||
schemaURL, ok := fc.String()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read scope span schema url")
|
||||
}
|
||||
ss.SchemaURL = strings.Clone(schemaURL)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InstrumentationScope is a message representing the instrumentation scope information
|
||||
// such as the fully qualified name and version.
|
||||
//
|
||||
// https://github.com/open-telemetry/opentelemetry-proto/blob/v1.5.0/opentelemetry/proto/common/v1/common.proto#L71
|
||||
// https://github.com/open-telemetry/opentelemetry-collector/blob/v0.124.0/pdata/internal/data/protogen/common/v1/common.pb.go#L340
|
||||
type InstrumentationScope struct {
|
||||
Name string
|
||||
Version string
|
||||
Attributes []*KeyValue
|
||||
DroppedAttributesCount uint32
|
||||
}
|
||||
|
||||
func (is *InstrumentationScope) marshalProtobuf(mm *easyproto.MessageMarshaler) {
|
||||
//message InstrumentationScope {
|
||||
// string name = 1;
|
||||
// string version = 2;
|
||||
// repeated KeyValue attributes = 3;
|
||||
// uint32 dropped_attributes_count = 4;
|
||||
//}
|
||||
mm.AppendString(1, is.Name)
|
||||
mm.AppendString(2, is.Version)
|
||||
for _, kv := range is.Attributes {
|
||||
kv.marshalProtobuf(mm.AppendMessage(3))
|
||||
}
|
||||
mm.AppendUint32(4, is.DroppedAttributesCount)
|
||||
}
|
||||
|
||||
func (is *InstrumentationScope) unmarshalProtobuf(src []byte) (err error) {
|
||||
var fc easyproto.FieldContext
|
||||
for len(src) > 0 {
|
||||
src, err = fc.NextField(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot read next field in Status: %w", err)
|
||||
}
|
||||
switch fc.FieldNum {
|
||||
case 1:
|
||||
name, ok := fc.String()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read scope name")
|
||||
}
|
||||
is.Name = strings.Clone(name)
|
||||
case 2:
|
||||
version, ok := fc.String()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read scope version")
|
||||
}
|
||||
is.Version = strings.Clone(version)
|
||||
case 3:
|
||||
data, ok := fc.MessageData()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read scope attributes data")
|
||||
}
|
||||
is.Attributes = append(is.Attributes, &KeyValue{})
|
||||
a := is.Attributes[len(is.Attributes)-1]
|
||||
if err := a.unmarshalProtobuf(data); err != nil {
|
||||
return fmt.Errorf("cannot unmarshal scope attribute: %w", err)
|
||||
}
|
||||
case 4:
|
||||
droppedAttributesCount, ok := fc.Uint32()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read scope dropped attributes count")
|
||||
}
|
||||
is.DroppedAttributesCount = droppedAttributesCount
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Span represents a single operation performed by a single component of the system.
|
||||
//
|
||||
// https://github.com/open-telemetry/opentelemetry-proto/blob/v1.5.0/opentelemetry/proto/trace/v1/trace.proto#L88
|
||||
// https://github.com/open-telemetry/opentelemetry-collector/blob/v0.124.0/pdata/internal/data/protogen/trace/v1/trace.pb.go#L380
|
||||
type Span struct {
|
||||
TraceID string
|
||||
SpanID string
|
||||
TraceState string
|
||||
ParentSpanID string
|
||||
Flags uint32
|
||||
Name string
|
||||
Kind SpanKind
|
||||
StartTimeUnixNano uint64
|
||||
EndTimeUnixNano uint64
|
||||
Attributes []*KeyValue
|
||||
DroppedAttributesCount uint32
|
||||
Events []*SpanEvent
|
||||
DroppedEventsCount uint32
|
||||
Links []*SpanLink
|
||||
DroppedLinksCount uint32
|
||||
Status Status
|
||||
}
|
||||
|
||||
func (s *Span) marshalProtobuf(mm *easyproto.MessageMarshaler) {
|
||||
//message Span {
|
||||
// bytes trace_id = 1;
|
||||
// bytes span_id = 2;
|
||||
// string trace_state = 3;
|
||||
// bytes parent_span_id = 4;
|
||||
// string name = 5;
|
||||
// SpanKind kind = 6;
|
||||
// fixed64 start_time_unix_nano = 7;
|
||||
// fixed64 end_time_unix_nano = 8;
|
||||
// repeated opentelemetry.proto.common.v1.KeyValue attributes = 9;
|
||||
// uint32 dropped_attributes_count = 10;
|
||||
// repeated Event events = 11;
|
||||
// uint32 dropped_events_count = 12;
|
||||
// repeated Link links = 13;
|
||||
// uint32 dropped_links_count = 14;
|
||||
// Status status = 15;
|
||||
//}
|
||||
traceID, err := hex.DecodeString(s.TraceID)
|
||||
if err != nil {
|
||||
traceID = []byte(s.TraceID)
|
||||
}
|
||||
mm.AppendBytes(1, traceID)
|
||||
|
||||
spanID, err := hex.DecodeString(s.SpanID)
|
||||
if err != nil {
|
||||
spanID = []byte(s.SpanID)
|
||||
}
|
||||
mm.AppendBytes(2, spanID)
|
||||
|
||||
mm.AppendString(3, s.TraceState)
|
||||
|
||||
parentSpanID, err := hex.DecodeString(s.ParentSpanID)
|
||||
if err != nil {
|
||||
parentSpanID = []byte(s.ParentSpanID)
|
||||
}
|
||||
mm.AppendBytes(4, parentSpanID)
|
||||
|
||||
mm.AppendString(5, s.Name)
|
||||
mm.AppendUint32(6, uint32(s.Kind))
|
||||
mm.AppendFixed64(7, s.StartTimeUnixNano)
|
||||
mm.AppendFixed64(8, s.EndTimeUnixNano)
|
||||
for _, a := range s.Attributes {
|
||||
a.marshalProtobuf(mm.AppendMessage(9))
|
||||
}
|
||||
mm.AppendUint32(10, s.DroppedAttributesCount)
|
||||
for _, e := range s.Events {
|
||||
e.marshalProtobuf(mm.AppendMessage(11))
|
||||
}
|
||||
mm.AppendUint32(12, s.DroppedEventsCount)
|
||||
for _, e := range s.Links {
|
||||
e.marshalProtobuf(mm.AppendMessage(13))
|
||||
}
|
||||
mm.AppendUint32(14, s.DroppedLinksCount)
|
||||
s.Status.marshalProtobuf(mm.AppendMessage(15))
|
||||
}
|
||||
|
||||
func (s *Span) unmarshalProtobuf(src []byte) (err error) {
|
||||
var fc easyproto.FieldContext
|
||||
for len(src) > 0 {
|
||||
src, err = fc.NextField(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot read next field in Status: %w", err)
|
||||
}
|
||||
switch fc.FieldNum {
|
||||
case 1:
|
||||
traceID, ok := fc.Bytes()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span trace id")
|
||||
}
|
||||
s.TraceID = hex.EncodeToString(traceID)
|
||||
case 2:
|
||||
spanID, ok := fc.Bytes()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span span id")
|
||||
}
|
||||
s.SpanID = hex.EncodeToString(spanID)
|
||||
case 3:
|
||||
traceState, ok := fc.String()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span trace state")
|
||||
}
|
||||
s.TraceState = strings.Clone(traceState)
|
||||
case 4:
|
||||
parentSpanID, ok := fc.Bytes()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span parent span id")
|
||||
}
|
||||
s.ParentSpanID = hex.EncodeToString(parentSpanID)
|
||||
case 5:
|
||||
name, ok := fc.String()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span name")
|
||||
}
|
||||
s.Name = strings.Clone(name)
|
||||
case 6:
|
||||
kind, ok := fc.Int32()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span kind")
|
||||
}
|
||||
s.Kind = SpanKind(kind)
|
||||
case 7:
|
||||
startTimeUnixNano, ok := fc.Fixed64()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span start timestamp")
|
||||
}
|
||||
s.StartTimeUnixNano = startTimeUnixNano
|
||||
case 8:
|
||||
endTimeUnixNano, ok := fc.Fixed64()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span end timestamp")
|
||||
}
|
||||
s.EndTimeUnixNano = endTimeUnixNano
|
||||
case 9:
|
||||
data, ok := fc.MessageData()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span attributes data")
|
||||
}
|
||||
s.Attributes = append(s.Attributes, &KeyValue{})
|
||||
a := s.Attributes[len(s.Attributes)-1]
|
||||
if err := a.unmarshalProtobuf(data); err != nil {
|
||||
return fmt.Errorf("cannot unmarshal span attribute: %w", err)
|
||||
}
|
||||
case 10:
|
||||
droppedAttributesCount, ok := fc.Uint32()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span dropped attributes count")
|
||||
}
|
||||
s.DroppedAttributesCount = droppedAttributesCount
|
||||
case 11:
|
||||
data, ok := fc.MessageData()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span event data")
|
||||
}
|
||||
s.Events = append(s.Events, &SpanEvent{})
|
||||
a := s.Events[len(s.Events)-1]
|
||||
if err = a.unmarshalProtobuf(data); err != nil {
|
||||
return fmt.Errorf("cannot unmarshal span event: %w", err)
|
||||
}
|
||||
case 12:
|
||||
droppedEventsCount, ok := fc.Uint32()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span dropped events count")
|
||||
}
|
||||
s.DroppedEventsCount = droppedEventsCount
|
||||
|
||||
case 13:
|
||||
data, ok := fc.MessageData()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span links data")
|
||||
}
|
||||
s.Links = append(s.Links, &SpanLink{})
|
||||
a := s.Links[len(s.Links)-1]
|
||||
if err = a.unmarshalProtobuf(data); err != nil {
|
||||
return fmt.Errorf("cannot unmarshal span link: %w", err)
|
||||
}
|
||||
case 14:
|
||||
droppedLinksCount, ok := fc.Uint32()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span dropped links count")
|
||||
}
|
||||
s.DroppedLinksCount = droppedLinksCount
|
||||
case 15:
|
||||
data, ok := fc.MessageData()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span status data")
|
||||
}
|
||||
if err = s.Status.unmarshalProtobuf(data); err != nil {
|
||||
return fmt.Errorf("cannot unmarshal span status: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SpanEvent is a time-stamped annotation of the span, consisting of user-supplied
|
||||
// text description and key-value pairs.
|
||||
//
|
||||
// https://github.com/open-telemetry/opentelemetry-proto/blob/v1.5.0/opentelemetry/proto/trace/v1/trace.proto#L222
|
||||
// https://github.com/open-telemetry/opentelemetry-collector/blob/v0.124.0/pdata/internal/data/protogen/trace/v1/trace.pb.go#L613
|
||||
type SpanEvent struct {
|
||||
TimeUnixNano uint64
|
||||
Name string
|
||||
Attributes []*KeyValue
|
||||
DroppedAttributesCount uint32
|
||||
}
|
||||
|
||||
func (se *SpanEvent) marshalProtobuf(mm *easyproto.MessageMarshaler) {
|
||||
//message Event {
|
||||
// fixed64 time_unix_nano = 1;
|
||||
// string name = 2;
|
||||
// repeated opentelemetry.proto.common.v1.KeyValue attributes = 3;
|
||||
// uint32 dropped_attributes_count = 4;
|
||||
//}
|
||||
mm.AppendFixed64(1, se.TimeUnixNano)
|
||||
mm.AppendString(2, se.Name)
|
||||
for _, a := range se.Attributes {
|
||||
a.marshalProtobuf(mm.AppendMessage(3))
|
||||
}
|
||||
mm.AppendUint32(4, se.DroppedAttributesCount)
|
||||
}
|
||||
|
||||
func (se *SpanEvent) unmarshalProtobuf(src []byte) (err error) {
|
||||
var fc easyproto.FieldContext
|
||||
for len(src) > 0 {
|
||||
src, err = fc.NextField(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot read next field in Status: %w", err)
|
||||
}
|
||||
switch fc.FieldNum {
|
||||
case 1:
|
||||
ts, ok := fc.Fixed64()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span event timestamp")
|
||||
}
|
||||
se.TimeUnixNano = ts
|
||||
case 2:
|
||||
name, ok := fc.String()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span event name")
|
||||
}
|
||||
se.Name = strings.Clone(name)
|
||||
case 3:
|
||||
data, ok := fc.MessageData()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span event attributes data")
|
||||
}
|
||||
se.Attributes = append(se.Attributes, &KeyValue{})
|
||||
a := se.Attributes[len(se.Attributes)-1]
|
||||
if err := a.unmarshalProtobuf(data); err != nil {
|
||||
return fmt.Errorf("cannot unmarshal span event attribute: %w", err)
|
||||
}
|
||||
case 4:
|
||||
droppedAttributesCount, ok := fc.Uint32()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span event dropped attributes count")
|
||||
}
|
||||
se.DroppedAttributesCount = droppedAttributesCount
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SpanLink is a pointer from the current span to another span in the same trace or in a
|
||||
// different trace. For example, this can be used in batching operations,
|
||||
// where a single batch handler processes multiple requests from different
|
||||
// traces or when the handler receives a request from a different project.
|
||||
//
|
||||
// https://github.com/open-telemetry/opentelemetry-proto/blob/v1.5.0/opentelemetry/proto/trace/v1/trace.proto#L251
|
||||
// https://github.com/open-telemetry/opentelemetry-collector/blob/v0.124.0/pdata/internal/data/protogen/trace/v1/trace.pb.go#L693
|
||||
type SpanLink struct {
|
||||
TraceID string
|
||||
SpanID string
|
||||
TraceState string
|
||||
Attributes []*KeyValue
|
||||
DroppedAttributesCount uint32
|
||||
Flags uint32
|
||||
}
|
||||
|
||||
func (sl *SpanLink) marshalProtobuf(mm *easyproto.MessageMarshaler) {
|
||||
//message Link {
|
||||
// bytes trace_id = 1;
|
||||
// bytes span_id = 2;
|
||||
// string trace_state = 3;
|
||||
// repeated opentelemetry.proto.common.v1.KeyValue attributes = 4;
|
||||
// uint32 dropped_attributes_count = 5;
|
||||
// fixed32 flags = 6;
|
||||
//}
|
||||
traceID, err := hex.DecodeString(sl.TraceID)
|
||||
if err != nil {
|
||||
traceID = []byte(sl.TraceID)
|
||||
}
|
||||
mm.AppendBytes(1, traceID)
|
||||
|
||||
spanID, err := hex.DecodeString(sl.SpanID)
|
||||
if err != nil {
|
||||
spanID = []byte(sl.SpanID)
|
||||
}
|
||||
mm.AppendBytes(2, spanID)
|
||||
|
||||
mm.AppendString(3, sl.TraceState)
|
||||
|
||||
for _, a := range sl.Attributes {
|
||||
a.marshalProtobuf(mm.AppendMessage(4))
|
||||
}
|
||||
mm.AppendUint32(5, sl.DroppedAttributesCount)
|
||||
mm.AppendFixed32(6, sl.Flags)
|
||||
}
|
||||
|
||||
func (sl *SpanLink) unmarshalProtobuf(src []byte) (err error) {
|
||||
var fc easyproto.FieldContext
|
||||
for len(src) > 0 {
|
||||
src, err = fc.NextField(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot read next field in Status: %w", err)
|
||||
}
|
||||
switch fc.FieldNum {
|
||||
case 1:
|
||||
traceID, ok := fc.Bytes()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span link trace id")
|
||||
}
|
||||
sl.TraceID = hex.EncodeToString(traceID)
|
||||
case 2:
|
||||
spanID, ok := fc.Bytes()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span link span id")
|
||||
}
|
||||
sl.SpanID = hex.EncodeToString(spanID)
|
||||
case 3:
|
||||
traceState, ok := fc.String()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span link trace state")
|
||||
}
|
||||
sl.TraceState = strings.Clone(traceState)
|
||||
case 4:
|
||||
data, ok := fc.MessageData()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read aspan link ttributes data")
|
||||
}
|
||||
sl.Attributes = append(sl.Attributes, &KeyValue{})
|
||||
a := sl.Attributes[len(sl.Attributes)-1]
|
||||
if err := a.unmarshalProtobuf(data); err != nil {
|
||||
return fmt.Errorf("cannot unmarshal span link attribute: %w", err)
|
||||
}
|
||||
case 5:
|
||||
droppedAttributesCount, ok := fc.Uint32()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span link dropped attributes count")
|
||||
}
|
||||
sl.DroppedAttributesCount = droppedAttributesCount
|
||||
case 6:
|
||||
flags, ok := fc.Fixed32()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read span link flags")
|
||||
}
|
||||
sl.Flags = flags
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// The Status type defines a logical error model that is suitable for different
|
||||
// programming environments, including REST APIs and RPC APIs.
|
||||
//
|
||||
// https://github.com/open-telemetry/opentelemetry-proto/blob/v1.5.0/opentelemetry/proto/trace/v1/trace.proto#L306
|
||||
// https://github.com/open-telemetry/opentelemetry-collector/blob/v0.124.0/pdata/internal/data/protogen/trace/v1/trace.pb.go#L791
|
||||
type Status struct {
|
||||
Message string
|
||||
Code StatusCode
|
||||
}
|
||||
|
||||
func (s *Status) marshalProtobuf(mm *easyproto.MessageMarshaler) {
|
||||
//message Status {
|
||||
// reserved 1;
|
||||
// string message = 2;
|
||||
// StatusCode code = 3;
|
||||
//}
|
||||
mm.AppendString(2, s.Message)
|
||||
mm.AppendInt32(3, int32(s.Code))
|
||||
}
|
||||
|
||||
func (s *Status) unmarshalProtobuf(src []byte) (err error) {
|
||||
var fc easyproto.FieldContext
|
||||
for len(src) > 0 {
|
||||
src, err = fc.NextField(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot read next field in Status: %w", err)
|
||||
}
|
||||
switch fc.FieldNum {
|
||||
case 2:
|
||||
message, ok := fc.String()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read status message")
|
||||
}
|
||||
s.Message = strings.Clone(message)
|
||||
case 3:
|
||||
code, ok := fc.Int32()
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot read status code")
|
||||
}
|
||||
s.Code = StatusCode(code)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user