Compare commits

...

7 Commits

Author SHA1 Message Date
Jiekun
a890a81ad9 feature: [victoriatraces] update victoria-traces docker image tag 2025-07-06 16:30:05 +08:00
Jiekun
4429ac3664 feature: [victoriatraces] add victoria-traces app 2025-07-06 16:24:03 +08:00
Jiekun
9364c1ee93 feature: [victoriatraces] add make and publish commands for victoriatraces 2025-07-06 16:06:12 +08:00
Zhu Jiekun
16975f413f VictoriaTraces: Fix ref type typo. Support resource attribute filter (#9379)
### Describe Your Changes

1. Currently user can't filter trace by resource attributes. This pull
request enable such capability when user use `resource_attr:` prefix in
tag filter of Jaeger query APIs.
2. Fix ref type typo `FOLLOWS_FROM`. This issue affect the visualization
in Grafana and Jaeger.

### Checklist

The following checks are **mandatory**:

- [x] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/#pull-request-checklist).
2025-07-06 15:49:24 +08:00
Zhu Jiekun
35d009fdd1 VictoriaTraces: add docker demo deployment (#9279)
### Describe Your Changes

Fix: https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9199

This pull request create docker compose for a demo setup of
VictoriaTraces.

To spin-up environment with VictoriaTraces run the following command:
```
make docker-vt-single-up
```
_See
[compose-vt-single.yml](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/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 [VictoriaLogs
datasource](https://github.com/VictoriaMetrics/victorialogs-datasource).

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/#pull-request-checklist).
2025-07-06 15:41:25 +08:00
Jiekun
f2ba60e3ee feature: [victoriatraces] sync master 2025-07-06 15:39:06 +08:00
Zhu Jiekun
960b1e8428 VictoriaLogs: Distributed Tracing Integration (#8988)
### Describe Your Changes

https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8148

This pull request:
1. Added support for OTLP trace endpoint
`/insert/opentelemetry/v1/traces` to VictoriaLogs.
2. Added Jaeger Query HTTP APIs `/select/jaeger/*` to  VictoriaLogs:
    1. `/select/jaeger/api/services`.
    2. `/select/jaeger/api/services/{service_name}/operations`.
    3. `/select/jaeger/api/traces`.
    4. `/select/jaeger/api/traces/{trace_id}`.
    5. `/select/jaeger/api/dependencies` (no-op / not implemented).
3. Added integration test for VictoriaLogs - Traces related ingestion
and query APIs.

It allows VictoriaLogs to ingest OTLP trace data and be queried as
Jaeger Query service by
[Grafana](https://grafana.com/docs/grafana/latest/datasources/jaeger/)
or [Jaeger Frontend](https://github.com/jaegertracing/jaeger-ui).

The folder structure of `app/vlselect/traces` is as follow:
```
traces
  - query (core logic of searching trace related data)
  - jaeger (call query and organize the result into the format jaeger need)
  - (tempo)
  - (zipkin)
  - (...)
```

### Checklist

The following checks are **mandatory**:

- [x] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).

---------

Co-authored-by: Phuong Le <39565248+func25@users.noreply.github.com>
Co-authored-by: Max Kotliar <mkotlyar@victoriametrics.com>
Co-authored-by: Max Kotliar <kotlyar.maksim@gmail.com>
2025-06-23 15:53:24 +08:00
34 changed files with 4159 additions and 150 deletions

View File

@@ -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

View 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

View 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
View 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)
}

View 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

View File

@@ -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"
)

View 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
}

View File

@@ -1,4 +1,4 @@
package opentelemetry
package logs
import (
"testing"

View File

@@ -1,4 +1,4 @@
package opentelemetry
package logs
import (
"fmt"

View File

@@ -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
}

View 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
}

View File

@@ -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

View 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
}

View 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 %}

View 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
}

View 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
}

View 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, "")
}

View 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
}

View 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)
}

View File

@@ -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

View File

@@ -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
}

View 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
View 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
View 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}...)
}

View File

@@ -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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

View 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: {}

View 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

View File

@@ -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
View 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)
}

View File

@@ -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()

View 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"
)

View 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
}