pkg/plugin/cloudtrace/cloudtrace.go (141 lines of code) (raw):
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cloudtrace
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"time"
"cloud.google.com/go/trace/apiv1/tracepb"
)
const (
servicePrefix = "service."
gaeServicePrefix = "g.co/gae/app/"
otelServiceKey = "service.name"
gaeServiceKey = "g.co/gae/app/module"
gaeServiceVersionKey = "g.co/gae/app/version"
otelMethodKey = "http.method"
cloudTraceMethodKey = "/http/method"
)
// Regex for individual filters within query text
var re = regexp.MustCompile(`(?:[^\s"]+|"(?:\\"|[^"])*")+`)
// TimeRange holds both a from and to time
type TimeRange struct {
From time.Time
To time.Time
}
// GetServiceName returns the service name for the span
func GetServiceName(span *tracepb.TraceSpan) string {
labels := span.GetLabels()
// In both cases treating "not existing" and "empty value" the same
serviceName := labels[otelServiceKey]
if serviceName == "" {
serviceName = labels[gaeServiceKey]
}
return serviceName
}
// GetTraceName gets the name, service label value, and method label value
// for the span and combines them to create a descriptive name
func GetTraceName(span *tracepb.TraceSpan) string {
namePart := span.GetName()
servicePart := GetServiceName(span)
if servicePart != "" {
servicePart = fmt.Sprintf("%s: ", servicePart)
}
methodPart := getHTTPMethod(span)
if methodPart != "" {
methodPart = fmt.Sprintf("HTTP %s ", methodPart)
}
return fmt.Sprintf("%s%s%s", servicePart, methodPart, namePart)
}
// GetSpanOperationName gets the name and method label value
// for the span and combines them to create a descriptive name
func GetSpanOperationName(span *tracepb.TraceSpan) string {
namePart := span.GetName()
methodPart := getHTTPMethod(span)
if methodPart != "" {
methodPart = fmt.Sprintf("HTTP %s ", methodPart)
}
return fmt.Sprintf("%s%s", methodPart, namePart)
}
// GetTags converts Google Trace labels to Grafana service and span tags
func GetTags(span *tracepb.TraceSpan) (serviceTags json.RawMessage, spanTags json.RawMessage, err error) {
spanLabels := span.GetLabels()
serviceTagsMapArray := []map[string]string{}
spanTagsMapArray := []map[string]string{}
for key, value := range spanLabels {
if strings.HasPrefix(key, servicePrefix) || strings.HasPrefix(key, gaeServicePrefix) {
serviceTagsMapArray = append(serviceTagsMapArray, map[string]string{"key": key, "value": value})
} else {
spanTagsMapArray = append(spanTagsMapArray, map[string]string{"key": key, "value": value})
}
}
serviceTags, err = json.Marshal(serviceTagsMapArray)
if err != nil {
return nil, nil, err
}
spanTags, err = json.Marshal(spanTagsMapArray)
if err != nil {
return nil, nil, err
}
return serviceTags, spanTags, nil
}
// GetListTracesFilter takes the raw query text from a user and converts it
// to a filter string as expected by the Cloud Trace API
func GetListTracesFilter(queryText string) (string, error) {
// Collect all filter parts from the query text
qTFilters := re.FindAllString(queryText, -1)
filters := make([]string, 0, len(qTFilters))
for _, qTFilter := range qTFilters {
key, value, err := getFilterKeyValue(qTFilter)
if err != nil {
return "", err
}
filters = append(filters, fmt.Sprintf("%s:%s", key, value))
}
return strings.Join(filters, " "), nil
}
func getHTTPMethod(span *tracepb.TraceSpan) string {
labels := span.GetLabels()
// In both cases treating "not existing" and "empty value" the same
httpMethod := labels[otelMethodKey]
if httpMethod == "" {
httpMethod = labels[cloudTraceMethodKey]
}
return httpMethod
}
func getFilterKeyValue(qTFilter string) (key string, value string, err error) {
// Filter part must be in form [key]:[value] from user
qTFilterParts := strings.SplitN(qTFilter, ":", 2)
if len(qTFilterParts) != 2 {
return "", "", fmt.Errorf("bad filter [%s]. Must be in form [key]:[value]", qTFilter)
}
key = qTFilterParts[0]
value = qTFilterParts[1]
// OR for generic labels filter must be in form LABEL:[key]:[value] from user
if strings.ToLower(key) == "label" {
qTFilterParts := strings.SplitN(value, ":", 2)
if len(qTFilterParts) != 2 {
return "", "", fmt.Errorf("bad filter [%s]. Must be in form LABEL:[key]:[value]", qTFilter)
}
// Cloud Trace API should not have "LABEL:" in filter
key = qTFilterParts[0]
value = qTFilterParts[1]
}
// Convert key to Cloud Trace API expected form if needed
switch key {
case "RootSpan":
key = "root"
case "SpanName":
key = "span"
case "HasLabel":
key = "label"
case "MinLatency":
key = "latency"
case "URL":
key = "url"
case "Method":
key = "method"
// Currently matches the Google Cloud Trace UI filter, but ignores "service.version" matches
case "Version":
key = gaeServiceVersionKey
// Currently matches the Google Cloud Trace UI filter, but ignores "service.name" matches
case "Service":
key = gaeServiceKey
case "Status":
key = "/http/status_code"
}
// If the value has less than 2 chars, no need to check for special filter chars
if len(value) < 2 {
return key, value, nil
}
firstChar := string(value[0])
secondChar := string(value[1])
// Move specials chars from the front of value to key for Google Cloud Trace compatibility
if (secondChar == "^" && firstChar == "+") || (secondChar == "+" && firstChar == "^") {
key = fmt.Sprintf("+^%s", key)
value = value[2:]
} else if firstChar == "+" || firstChar == "^" {
key = fmt.Sprintf("%s%s", firstChar, key)
value = value[1:]
}
return key, value, nil
}