mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-18 16:07:10 +00:00
## 📄 Summary
To reliably migrate the alerts and dashboards, we need access to the telemetrystore to fetch some metadata and while doing migration, I need to log some stuff to fix stuff later.
Key changes:
- Modified the migration to include telemetrystore and a logging provider (open to using a standard logger instead)
- To avoid the previous issues with imported dashboards failing during migration, I've ensured that imported JSON files are automatically transformed when migration is active
- Implemented detailed logic to handle dashboard migration cleanly and prevent unnecessary errors
- Separated the core migration logic from SQL migration code, as users from the dot metrics migration requested shareable code snippets for local migrations. This modular approach allows others to easily reuse the migration functionality.
Known: I didn't register the migration yet in this PR, and will not merge this yet, so please review with that in mid.
944 lines
23 KiB
Go
944 lines
23 KiB
Go
// nolint
|
|
package transition
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/SigNoz/signoz/pkg/telemetrytraces"
|
|
)
|
|
|
|
type migrateCommon struct {
|
|
ambiguity map[string][]string
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func (migration *migrateCommon) wrapInV5Envelope(name string, queryMap map[string]any, queryType string) map[string]any {
|
|
// Create a properly structured v5 query
|
|
v5Query := map[string]any{
|
|
"name": name,
|
|
"disabled": queryMap["disabled"],
|
|
"legend": queryMap["legend"],
|
|
}
|
|
|
|
if name != queryMap["expression"] {
|
|
// formula
|
|
queryType = "builder_formula"
|
|
v5Query["expression"] = queryMap["expression"]
|
|
if functions, ok := queryMap["functions"]; ok {
|
|
v5Query["functions"] = functions
|
|
}
|
|
return map[string]any{
|
|
"type": queryType,
|
|
"spec": v5Query,
|
|
}
|
|
}
|
|
|
|
// Add signal based on data source
|
|
if dataSource, ok := queryMap["dataSource"].(string); ok {
|
|
switch dataSource {
|
|
case "traces":
|
|
v5Query["signal"] = "traces"
|
|
case "logs":
|
|
v5Query["signal"] = "logs"
|
|
case "metrics":
|
|
v5Query["signal"] = "metrics"
|
|
}
|
|
}
|
|
|
|
if stepInterval, ok := queryMap["stepInterval"]; ok {
|
|
v5Query["stepInterval"] = stepInterval
|
|
}
|
|
|
|
if aggregations, ok := queryMap["aggregations"]; ok {
|
|
v5Query["aggregations"] = aggregations
|
|
}
|
|
|
|
if filter, ok := queryMap["filter"]; ok {
|
|
v5Query["filter"] = filter
|
|
}
|
|
|
|
// Copy groupBy with proper structure
|
|
if groupBy, ok := queryMap["groupBy"].([]any); ok {
|
|
v5GroupBy := make([]any, len(groupBy))
|
|
for i, gb := range groupBy {
|
|
if gbMap, ok := gb.(map[string]any); ok {
|
|
v5GroupBy[i] = map[string]any{
|
|
"name": gbMap["key"],
|
|
"fieldDataType": gbMap["dataType"],
|
|
"fieldContext": gbMap["type"],
|
|
}
|
|
}
|
|
}
|
|
v5Query["groupBy"] = v5GroupBy
|
|
}
|
|
|
|
// Copy orderBy with proper structure
|
|
if orderBy, ok := queryMap["orderBy"].([]any); ok {
|
|
v5OrderBy := make([]any, len(orderBy))
|
|
for i, ob := range orderBy {
|
|
if obMap, ok := ob.(map[string]any); ok {
|
|
v5OrderBy[i] = map[string]any{
|
|
"key": map[string]any{
|
|
"name": obMap["columnName"],
|
|
"fieldDataType": obMap["dataType"],
|
|
"fieldContext": obMap["type"],
|
|
},
|
|
"direction": obMap["order"],
|
|
}
|
|
}
|
|
}
|
|
v5Query["order"] = v5OrderBy
|
|
}
|
|
|
|
// Copy selectColumns as selectFields
|
|
if selectColumns, ok := queryMap["selectColumns"].([]any); ok {
|
|
v5SelectFields := make([]any, len(selectColumns))
|
|
for i, col := range selectColumns {
|
|
if colMap, ok := col.(map[string]any); ok {
|
|
v5SelectFields[i] = map[string]any{
|
|
"name": colMap["key"],
|
|
"fieldDataType": colMap["dataType"],
|
|
"fieldContext": colMap["type"],
|
|
}
|
|
}
|
|
}
|
|
v5Query["selectFields"] = v5SelectFields
|
|
}
|
|
|
|
// Copy limit and offset
|
|
if limit, ok := queryMap["limit"]; ok {
|
|
v5Query["limit"] = limit
|
|
}
|
|
if offset, ok := queryMap["offset"]; ok {
|
|
v5Query["offset"] = offset
|
|
}
|
|
|
|
if having, ok := queryMap["having"]; ok {
|
|
v5Query["having"] = having
|
|
}
|
|
|
|
if functions, ok := queryMap["functions"]; ok {
|
|
v5Query["functions"] = functions
|
|
}
|
|
|
|
return map[string]any{
|
|
"type": queryType,
|
|
"spec": v5Query,
|
|
}
|
|
}
|
|
|
|
func (mc *migrateCommon) updateQueryData(ctx context.Context, queryData map[string]any, version, widgetType string) bool {
|
|
updated := false
|
|
|
|
aggregateOp, _ := queryData["aggregateOperator"].(string)
|
|
hasAggregation := aggregateOp != "" && aggregateOp != "noop"
|
|
|
|
if mc.createAggregations(ctx, queryData, version, widgetType) {
|
|
updated = true
|
|
}
|
|
|
|
if mc.createFilterExpression(ctx, queryData) {
|
|
updated = true
|
|
}
|
|
|
|
if mc.fixGroupBy(queryData) {
|
|
updated = true
|
|
}
|
|
|
|
if mc.createHavingExpression(ctx, queryData) {
|
|
updated = true
|
|
}
|
|
|
|
if hasAggregation {
|
|
if orderBy, ok := queryData["orderBy"].([]any); ok {
|
|
newOrderBy := make([]any, 0)
|
|
for _, order := range orderBy {
|
|
if orderMap, ok := order.(map[string]any); ok {
|
|
columnName, _ := orderMap["columnName"].(string)
|
|
// skip timestamp, id (logs, traces), samples(metrics) ordering for aggregation queries
|
|
if columnName != "timestamp" && columnName != "samples" && columnName != "id" {
|
|
if columnName == "#SIGNOZ_VALUE" {
|
|
if expr, has := mc.orderByExpr(queryData); has {
|
|
orderMap["columnName"] = expr
|
|
}
|
|
} else {
|
|
// if the order by key is not part of the group by keys, remove it
|
|
present := false
|
|
|
|
groupBy, ok := queryData["groupBy"].([]any)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
for idx := range groupBy {
|
|
item, ok := groupBy[idx].(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
key, ok := item["key"].(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if key == columnName {
|
|
present = true
|
|
}
|
|
}
|
|
|
|
if !present {
|
|
mc.logger.WarnContext(ctx, "found a order by without group by, skipping", "order_col_name", columnName)
|
|
continue
|
|
}
|
|
}
|
|
newOrderBy = append(newOrderBy, orderMap)
|
|
}
|
|
}
|
|
}
|
|
queryData["orderBy"] = newOrderBy
|
|
updated = true
|
|
}
|
|
} else {
|
|
dataSource, _ := queryData["dataSource"].(string)
|
|
|
|
if orderBy, ok := queryData["orderBy"].([]any); ok {
|
|
newOrderBy := make([]any, 0)
|
|
for _, order := range orderBy {
|
|
if orderMap, ok := order.(map[string]any); ok {
|
|
columnName, _ := orderMap["columnName"].(string)
|
|
// skip id and timestamp for (traces)
|
|
if (columnName == "id" || columnName == "timestamp") && dataSource == "traces" {
|
|
mc.logger.InfoContext(ctx, "skipping `id` order by for traces")
|
|
continue
|
|
}
|
|
|
|
// skip id for (logs)
|
|
if (columnName == "id" || columnName == "timestamp") && dataSource == "logs" {
|
|
mc.logger.InfoContext(ctx, "skipping `id`/`timestamp` order by for logs")
|
|
continue
|
|
}
|
|
|
|
newOrderBy = append(newOrderBy, orderMap)
|
|
}
|
|
}
|
|
queryData["orderBy"] = newOrderBy
|
|
updated = true
|
|
}
|
|
}
|
|
|
|
if functions, ok := queryData["functions"].([]any); ok {
|
|
v5Functions := make([]any, len(functions))
|
|
for i, fn := range functions {
|
|
if fnMap, ok := fn.(map[string]any); ok {
|
|
v5Function := map[string]any{
|
|
"name": fnMap["name"],
|
|
}
|
|
|
|
// Convert args from v4 format to v5 FunctionArg format
|
|
if args, ok := fnMap["args"].([]any); ok {
|
|
v5Args := make([]any, len(args))
|
|
for j, arg := range args {
|
|
// In v4, args were just values. In v5, they are FunctionArg objects
|
|
v5Args[j] = map[string]any{
|
|
"name": "", // v4 didn't have named args
|
|
"value": arg,
|
|
}
|
|
}
|
|
v5Function["args"] = v5Args
|
|
}
|
|
|
|
// Handle namedArgs if present (some functions might have used this)
|
|
if namedArgs, ok := fnMap["namedArgs"].(map[string]any); ok {
|
|
// Convert named args to the new format
|
|
existingArgs, _ := v5Function["args"].([]any)
|
|
if existingArgs == nil {
|
|
existingArgs = []any{}
|
|
}
|
|
|
|
for name, value := range namedArgs {
|
|
existingArgs = append(existingArgs, map[string]any{
|
|
"name": name,
|
|
"value": value,
|
|
})
|
|
}
|
|
v5Function["args"] = existingArgs
|
|
}
|
|
|
|
v5Functions[i] = v5Function
|
|
}
|
|
}
|
|
queryData["functions"] = v5Functions
|
|
updated = true
|
|
}
|
|
|
|
delete(queryData, "aggregateOperator")
|
|
delete(queryData, "aggregateAttribute")
|
|
delete(queryData, "temporality")
|
|
delete(queryData, "timeAggregation")
|
|
delete(queryData, "spaceAggregation")
|
|
delete(queryData, "reduceTo")
|
|
delete(queryData, "filters")
|
|
delete(queryData, "ShiftBy")
|
|
delete(queryData, "IsAnomaly")
|
|
delete(queryData, "QueriesUsedInFormula")
|
|
delete(queryData, "seriesAggregation")
|
|
|
|
return updated
|
|
}
|
|
|
|
func (mc *migrateCommon) orderByExpr(queryData map[string]any) (string, bool) {
|
|
aggregateOp, hasOp := queryData["aggregateOperator"].(string)
|
|
aggregateAttr, hasAttr := queryData["aggregateAttribute"].(map[string]any)
|
|
dataSource, _ := queryData["dataSource"].(string)
|
|
|
|
if aggregateOp == "noop" {
|
|
return "", false
|
|
}
|
|
|
|
if !hasOp || !hasAttr {
|
|
return "", false
|
|
}
|
|
|
|
var expr string
|
|
var has bool
|
|
|
|
switch dataSource {
|
|
case "metrics":
|
|
|
|
aggs, ok := queryData["aggregations"].([]any)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
|
|
if len(aggs) == 0 {
|
|
return "", false
|
|
}
|
|
|
|
agg, ok := aggs[0].(map[string]any)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
|
|
spaceAgg, ok := agg["spaceAggregation"].(string)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
|
|
expr = fmt.Sprintf("%s(%s)", spaceAgg, aggregateAttr["key"])
|
|
has = true
|
|
case "logs":
|
|
expr = mc.buildAggregationExpression(aggregateOp, aggregateAttr)
|
|
has = true
|
|
case "traces":
|
|
expr = mc.buildAggregationExpression(aggregateOp, aggregateAttr)
|
|
has = true
|
|
default:
|
|
has = false
|
|
}
|
|
|
|
return expr, has
|
|
}
|
|
|
|
func (mc *migrateCommon) createAggregations(ctx context.Context, queryData map[string]any, version, widgetType string) bool {
|
|
aggregateOp, hasOp := queryData["aggregateOperator"].(string)
|
|
aggregateAttr, hasAttr := queryData["aggregateAttribute"].(map[string]any)
|
|
dataSource, _ := queryData["dataSource"].(string)
|
|
|
|
if aggregateOp == "noop" {
|
|
return false
|
|
}
|
|
|
|
if !hasOp || !hasAttr {
|
|
return false
|
|
}
|
|
|
|
var aggregation map[string]any
|
|
|
|
switch dataSource {
|
|
case "metrics":
|
|
if version == "v4" {
|
|
if _, ok := queryData["spaceAggregation"]; !ok {
|
|
queryData["spaceAggregation"] = aggregateOp
|
|
}
|
|
aggregation = map[string]any{
|
|
"metricName": aggregateAttr["key"],
|
|
"temporality": queryData["temporality"],
|
|
"timeAggregation": aggregateOp,
|
|
"spaceAggregation": queryData["spaceAggregation"],
|
|
}
|
|
if reduceTo, ok := queryData["reduceTo"].(string); ok {
|
|
aggregation["reduceTo"] = reduceTo
|
|
}
|
|
} else {
|
|
var timeAgg, spaceAgg, reduceTo string
|
|
switch aggregateOp {
|
|
case "sum_rate", "rate_sum":
|
|
timeAgg = "rate"
|
|
spaceAgg = "sum"
|
|
reduceTo = "sum"
|
|
case "avg_rate", "rate_avg":
|
|
timeAgg = "rate"
|
|
spaceAgg = "avg"
|
|
reduceTo = "avg"
|
|
case "min_rate", "rate_min":
|
|
timeAgg = "rate"
|
|
spaceAgg = "min"
|
|
reduceTo = "min"
|
|
case "max_rate", "rate_max":
|
|
timeAgg = "rate"
|
|
spaceAgg = "max"
|
|
reduceTo = "max"
|
|
case "hist_quantile_50":
|
|
timeAgg = ""
|
|
spaceAgg = "p50"
|
|
reduceTo = "avg"
|
|
case "hist_quantile_75":
|
|
timeAgg = ""
|
|
spaceAgg = "p75"
|
|
reduceTo = "avg"
|
|
case "hist_quantile_90":
|
|
timeAgg = ""
|
|
spaceAgg = "p90"
|
|
reduceTo = "avg"
|
|
case "hist_quantile_95":
|
|
timeAgg = ""
|
|
spaceAgg = "p95"
|
|
reduceTo = "avg"
|
|
case "hist_quantile_99":
|
|
timeAgg = ""
|
|
spaceAgg = "p99"
|
|
reduceTo = "avg"
|
|
case "rate":
|
|
timeAgg = "rate"
|
|
spaceAgg = "sum"
|
|
reduceTo = "sum"
|
|
case "p99", "p90", "p75", "p50", "p25", "p20", "p10", "p05":
|
|
mc.logger.InfoContext(ctx, "found invalid config")
|
|
timeAgg = "avg"
|
|
spaceAgg = "avg"
|
|
reduceTo = "avg"
|
|
case "min":
|
|
timeAgg = "min"
|
|
spaceAgg = "min"
|
|
reduceTo = "min"
|
|
case "max":
|
|
timeAgg = "max"
|
|
spaceAgg = "max"
|
|
reduceTo = "max"
|
|
case "avg":
|
|
timeAgg = "avg"
|
|
spaceAgg = "avg"
|
|
reduceTo = "avg"
|
|
case "sum":
|
|
timeAgg = "sum"
|
|
spaceAgg = "sum"
|
|
reduceTo = "sum"
|
|
case "count":
|
|
timeAgg = "count"
|
|
spaceAgg = "sum"
|
|
reduceTo = "sum"
|
|
case "count_distinct":
|
|
timeAgg = "count_distinct"
|
|
spaceAgg = "sum"
|
|
reduceTo = "sum"
|
|
case "noop":
|
|
mc.logger.WarnContext(ctx, "noop found in the aggregation data")
|
|
timeAgg = "max"
|
|
spaceAgg = "max"
|
|
reduceTo = "max"
|
|
}
|
|
|
|
aggregation = map[string]any{
|
|
"metricName": aggregateAttr["key"],
|
|
"temporality": queryData["temporality"],
|
|
"timeAggregation": timeAgg,
|
|
"spaceAggregation": spaceAgg,
|
|
}
|
|
if widgetType == "table" {
|
|
aggregation["reduceTo"] = reduceTo
|
|
} else {
|
|
if reduceTo, ok := queryData["reduceTo"].(string); ok {
|
|
aggregation["reduceTo"] = reduceTo
|
|
}
|
|
}
|
|
}
|
|
|
|
case "logs":
|
|
expression := mc.buildAggregationExpression(aggregateOp, aggregateAttr)
|
|
aggregation = map[string]any{
|
|
"expression": expression,
|
|
}
|
|
case "traces":
|
|
expression := mc.buildAggregationExpression(aggregateOp, aggregateAttr)
|
|
aggregation = map[string]any{
|
|
"expression": expression,
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
|
|
queryData["aggregations"] = []any{aggregation}
|
|
|
|
return true
|
|
}
|
|
|
|
func (mc *migrateCommon) createFilterExpression(ctx context.Context, queryData map[string]any) bool {
|
|
filters, ok := queryData["filters"].(map[string]any)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
items, ok := filters["items"].([]any)
|
|
if !ok || len(items) == 0 {
|
|
return false
|
|
}
|
|
|
|
op, ok := filters["op"].(string)
|
|
if !ok {
|
|
op = "AND"
|
|
}
|
|
|
|
dataSource, _ := queryData["dataSource"].(string)
|
|
|
|
expression := mc.buildExpression(ctx, items, op, dataSource)
|
|
if expression != "" {
|
|
if groupByExists := mc.groupByExistsExpr(queryData); groupByExists != "" && dataSource != "metrics" {
|
|
mc.logger.InfoContext(ctx, "adding default exists for old qb", "group_by_exists", groupByExists)
|
|
expression += " " + groupByExists
|
|
}
|
|
|
|
queryData["filter"] = map[string]any{
|
|
"expression": expression,
|
|
}
|
|
delete(queryData, "filters")
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (mc *migrateCommon) groupByExistsExpr(queryData map[string]any) string {
|
|
expr := []string{}
|
|
groupBy, ok := queryData["groupBy"].([]any)
|
|
if !ok {
|
|
return strings.Join(expr, " AND ")
|
|
}
|
|
|
|
for idx := range groupBy {
|
|
item, ok := groupBy[idx].(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
key, ok := item["key"].(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
expr = append(expr, fmt.Sprintf("%s EXISTS", key))
|
|
|
|
if _, ok := telemetrytraces.IntrinsicFields[key]; ok {
|
|
delete(item, "type")
|
|
}
|
|
if _, ok := telemetrytraces.CalculatedFields[key]; ok {
|
|
delete(item, "type")
|
|
}
|
|
if _, ok := telemetrytraces.IntrinsicFieldsDeprecated[key]; ok {
|
|
delete(item, "type")
|
|
}
|
|
if _, ok := telemetrytraces.CalculatedFieldsDeprecated[key]; ok {
|
|
delete(item, "type")
|
|
}
|
|
}
|
|
|
|
return strings.Join(expr, " AND ")
|
|
}
|
|
|
|
func (mc *migrateCommon) fixGroupBy(queryData map[string]any) bool {
|
|
groupBy, ok := queryData["groupBy"].([]any)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
for idx := range groupBy {
|
|
item, ok := groupBy[idx].(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
key, ok := item["key"].(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if _, ok := telemetrytraces.IntrinsicFields[key]; ok {
|
|
delete(item, "type")
|
|
}
|
|
if _, ok := telemetrytraces.CalculatedFields[key]; ok {
|
|
delete(item, "type")
|
|
}
|
|
if _, ok := telemetrytraces.IntrinsicFieldsDeprecated[key]; ok {
|
|
delete(item, "type")
|
|
}
|
|
if _, ok := telemetrytraces.CalculatedFieldsDeprecated[key]; ok {
|
|
delete(item, "type")
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (mc *migrateCommon) createHavingExpression(ctx context.Context, queryData map[string]any) bool {
|
|
having, ok := queryData["having"].([]any)
|
|
if !ok || len(having) == 0 {
|
|
queryData["having"] = map[string]any{
|
|
"expression": "",
|
|
}
|
|
return true
|
|
}
|
|
|
|
dataSource, _ := queryData["dataSource"].(string)
|
|
|
|
for idx := range having {
|
|
if havingItem, ok := having[idx].(map[string]any); ok {
|
|
havingCol, has := mc.orderByExpr(queryData)
|
|
if has {
|
|
havingItem["columnName"] = havingCol
|
|
havingItem["key"] = map[string]any{"key": havingCol}
|
|
}
|
|
having[idx] = havingItem
|
|
}
|
|
}
|
|
|
|
mc.logger.InfoContext(ctx, "having before expression", "having", having)
|
|
|
|
expression := mc.buildExpression(ctx, having, "AND", dataSource)
|
|
mc.logger.InfoContext(ctx, "having expression after building", "expression", expression, "having", having)
|
|
queryData["having"] = map[string]any{
|
|
"expression": expression,
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (mc *migrateCommon) buildExpression(ctx context.Context, items []any, op, dataSource string) string {
|
|
if len(items) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var conditions []string
|
|
|
|
for _, item := range items {
|
|
itemMap, ok := item.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
key, keyOk := itemMap["key"].(map[string]any)
|
|
operator, opOk := itemMap["op"].(string)
|
|
value, valueOk := itemMap["value"]
|
|
|
|
if !keyOk || !opOk || !valueOk {
|
|
mc.logger.WarnContext(ctx, "didn't find either key, op, or value; continuing")
|
|
continue
|
|
}
|
|
|
|
keyStr, ok := key["key"].(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if slices.Contains(mc.ambiguity[dataSource], keyStr) {
|
|
mc.logger.WarnContext(ctx, "ambiguity found for a key", "ambiguity_key", keyStr)
|
|
typeStr, ok := key["type"].(string)
|
|
if ok {
|
|
if typeStr == "tag" {
|
|
typeStr = "attribute"
|
|
} else {
|
|
typeStr = "resource"
|
|
}
|
|
keyStr = typeStr + "." + keyStr
|
|
}
|
|
}
|
|
|
|
condition := mc.buildCondition(ctx, keyStr, operator, value, key)
|
|
if condition != "" {
|
|
conditions = append(conditions, condition)
|
|
}
|
|
|
|
}
|
|
|
|
if len(conditions) == 0 {
|
|
return ""
|
|
}
|
|
|
|
if len(conditions) == 1 {
|
|
return conditions[0]
|
|
}
|
|
|
|
return "(" + strings.Join(conditions, " "+op+" ") + ")"
|
|
}
|
|
|
|
func (mc *migrateCommon) buildCondition(ctx context.Context, key, operator string, value any, keyMetadata map[string]any) string {
|
|
dataType, _ := keyMetadata["dataType"].(string)
|
|
|
|
formattedValue := mc.formatValue(ctx, value, dataType)
|
|
|
|
switch operator {
|
|
case "=":
|
|
return fmt.Sprintf("%s = %s", key, formattedValue)
|
|
case "!=":
|
|
return fmt.Sprintf("%s != %s", key, formattedValue)
|
|
case ">":
|
|
return fmt.Sprintf("%s > %s", key, formattedValue)
|
|
case ">=":
|
|
return fmt.Sprintf("%s >= %s", key, formattedValue)
|
|
case "<":
|
|
return fmt.Sprintf("%s < %s", key, formattedValue)
|
|
case "<=":
|
|
return fmt.Sprintf("%s <= %s", key, formattedValue)
|
|
case "in", "IN":
|
|
return fmt.Sprintf("%s IN %s", key, formattedValue)
|
|
case "nin", "NOT IN":
|
|
return fmt.Sprintf("%s NOT IN %s", key, formattedValue)
|
|
case "like", "LIKE":
|
|
return fmt.Sprintf("%s LIKE %s", key, formattedValue)
|
|
case "nlike", "NOT LIKE":
|
|
return fmt.Sprintf("%s NOT LIKE %s", key, formattedValue)
|
|
case "contains":
|
|
return fmt.Sprintf("%s CONTAINS %s", key, formattedValue)
|
|
case "ncontains":
|
|
return fmt.Sprintf("%s NOT CONTAINS %s", key, formattedValue)
|
|
case "regex":
|
|
return fmt.Sprintf("%s REGEXP %s", key, formattedValue)
|
|
case "nregex":
|
|
return fmt.Sprintf("%s NOT REGEXP %s", key, formattedValue)
|
|
case "exists":
|
|
return fmt.Sprintf("%s EXISTS", key)
|
|
case "nexists":
|
|
return fmt.Sprintf("%s NOT EXISTS", key)
|
|
case "has":
|
|
return fmt.Sprintf("has(%s, %s)", key, formattedValue)
|
|
case "nhas":
|
|
return fmt.Sprintf("NOT has(%s, %s)", key, formattedValue)
|
|
default:
|
|
return fmt.Sprintf("%s %s %s", key, operator, formattedValue)
|
|
}
|
|
}
|
|
|
|
func (mc *migrateCommon) buildAggregationExpression(operator string, attribute map[string]any) string {
|
|
key, _ := attribute["key"].(string)
|
|
|
|
switch operator {
|
|
case "count":
|
|
return "count()"
|
|
case "sum":
|
|
if key != "" {
|
|
return fmt.Sprintf("sum(%s)", key)
|
|
}
|
|
return "sum()"
|
|
case "avg":
|
|
if key != "" {
|
|
return fmt.Sprintf("avg(%s)", key)
|
|
}
|
|
return "avg()"
|
|
case "min":
|
|
if key != "" {
|
|
return fmt.Sprintf("min(%s)", key)
|
|
}
|
|
return "min()"
|
|
case "max":
|
|
if key != "" {
|
|
return fmt.Sprintf("max(%s)", key)
|
|
}
|
|
return "max()"
|
|
case "p05":
|
|
if key != "" {
|
|
return fmt.Sprintf("p05(%s)", key)
|
|
}
|
|
return "p05()"
|
|
case "p10":
|
|
if key != "" {
|
|
return fmt.Sprintf("p10(%s)", key)
|
|
}
|
|
return "p10()"
|
|
case "p20":
|
|
if key != "" {
|
|
return fmt.Sprintf("p20(%s)", key)
|
|
}
|
|
return "p20()"
|
|
case "p25":
|
|
if key != "" {
|
|
return fmt.Sprintf("p25(%s)", key)
|
|
}
|
|
return "p25()"
|
|
case "p50":
|
|
if key != "" {
|
|
return fmt.Sprintf("p50(%s)", key)
|
|
}
|
|
return "p50()"
|
|
case "p90":
|
|
if key != "" {
|
|
return fmt.Sprintf("p90(%s)", key)
|
|
}
|
|
return "p90()"
|
|
case "p95":
|
|
if key != "" {
|
|
return fmt.Sprintf("p95(%s)", key)
|
|
}
|
|
return "p95()"
|
|
case "p99":
|
|
if key != "" {
|
|
return fmt.Sprintf("p99(%s)", key)
|
|
}
|
|
return "p99()"
|
|
case "rate":
|
|
if key != "" {
|
|
return fmt.Sprintf("rate(%s)", key)
|
|
}
|
|
return "rate()"
|
|
case "rate_sum":
|
|
if key != "" {
|
|
return fmt.Sprintf("rate_sum(%s)", key)
|
|
}
|
|
return "rate_sum()"
|
|
case "rate_avg":
|
|
if key != "" {
|
|
return fmt.Sprintf("rate_avg(%s)", key)
|
|
}
|
|
return "rate_avg()"
|
|
case "rate_min":
|
|
if key != "" {
|
|
return fmt.Sprintf("rate_min(%s)", key)
|
|
}
|
|
return "rate_min()"
|
|
case "rate_max":
|
|
if key != "" {
|
|
return fmt.Sprintf("rate_max(%s)", key)
|
|
}
|
|
return "rate_max()"
|
|
case "sum_rate":
|
|
if key != "" {
|
|
return fmt.Sprintf("sum(rate(%s))", key)
|
|
}
|
|
return "sum(rate())"
|
|
case "count_distinct":
|
|
if key != "" {
|
|
return fmt.Sprintf("count_distinct(%s)", key)
|
|
}
|
|
return "count_distinct()"
|
|
default:
|
|
// For unknown operators, try to use them as-is
|
|
if key != "" {
|
|
return fmt.Sprintf("%s(%s)", operator, key)
|
|
}
|
|
return fmt.Sprintf("%s()", operator)
|
|
}
|
|
}
|
|
|
|
func (mc *migrateCommon) formatValue(ctx context.Context, value any, dataType string) string {
|
|
switch v := value.(type) {
|
|
case string:
|
|
if mc.isVariable(v) {
|
|
mc.logger.InfoContext(ctx, "found a variable", "dashboard_variable", v)
|
|
return mc.normalizeVariable(ctx, v)
|
|
} else {
|
|
// if we didn't recognize something as variable but looks like has variable like value, double check
|
|
if strings.Contains(v, "{") || strings.Contains(v, "[") || strings.Contains(v, "$") {
|
|
mc.logger.WarnContext(ctx, "variable like string found", "dashboard_variable", v)
|
|
}
|
|
}
|
|
|
|
if mc.isNumericType(dataType) {
|
|
if _, err := fmt.Sscanf(v, "%f", new(float64)); err == nil {
|
|
return v // Return the numeric string without quotes
|
|
}
|
|
}
|
|
|
|
// Otherwise, it's a string literal - escape single quotes and wrap in quotes
|
|
escaped := strings.ReplaceAll(v, "'", "\\'")
|
|
return fmt.Sprintf("'%s'", escaped)
|
|
case float64:
|
|
return fmt.Sprintf("%v", v)
|
|
case int:
|
|
return fmt.Sprintf("%d", v)
|
|
case bool:
|
|
return fmt.Sprintf("%t", v)
|
|
case []any:
|
|
if len(v) == 1 {
|
|
return mc.formatValue(ctx, v[0], dataType)
|
|
}
|
|
var values []string
|
|
for _, item := range v {
|
|
values = append(values, mc.formatValue(ctx, item, dataType))
|
|
}
|
|
return "[" + strings.Join(values, ", ") + "]"
|
|
default:
|
|
return fmt.Sprintf("%v", v)
|
|
}
|
|
}
|
|
|
|
func (mc *migrateCommon) isNumericType(dataType string) bool {
|
|
switch dataType {
|
|
case "int", "int8", "int16", "int32", "int64",
|
|
"uint", "uint8", "uint16", "uint32", "uint64",
|
|
"float", "float32", "float64",
|
|
"number", "numeric", "integer":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (mc *migrateCommon) isVariable(s string) bool {
|
|
s = strings.TrimSpace(s)
|
|
|
|
patterns := []string{
|
|
`^\{\{.*\}\}$`, // {{var}} or {{.var}}
|
|
`^\$.*$`, // $var or $service.name
|
|
`^\[\[.*\]\]$`, // [[var]] or [[.var]]
|
|
`^\$\{\{.*\}\}$`, // ${{env}} or ${{.var}}
|
|
}
|
|
|
|
for _, pattern := range patterns {
|
|
matched, _ := regexp.MatchString(pattern, s)
|
|
if matched {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (mc *migrateCommon) normalizeVariable(ctx context.Context, s string) string {
|
|
s = strings.TrimSpace(s)
|
|
|
|
var varName string
|
|
|
|
// {{var}} or {{.var}}
|
|
if strings.HasPrefix(s, "{{") && strings.HasSuffix(s, "}}") {
|
|
varName = strings.TrimPrefix(strings.TrimSuffix(s, "}}"), "{{")
|
|
varName = strings.TrimPrefix(varName, ".")
|
|
// this is probably going to be problem if user has $ as start of key
|
|
varName = strings.TrimPrefix(varName, "$")
|
|
} else if strings.HasPrefix(s, "[[") && strings.HasSuffix(s, "]]") {
|
|
// [[var]] or [[.var]]
|
|
varName = strings.TrimPrefix(strings.TrimSuffix(s, "]]"), "[[")
|
|
varName = strings.TrimPrefix(varName, ".")
|
|
} else if strings.HasPrefix(s, "${{") && strings.HasSuffix(s, "}}") {
|
|
varName = strings.TrimPrefix(strings.TrimSuffix(s, "}}"), "${{")
|
|
varName = strings.TrimPrefix(varName, ".")
|
|
varName = strings.TrimPrefix(varName, "$")
|
|
} else if strings.HasPrefix(s, "$") {
|
|
// $var
|
|
return s
|
|
} else {
|
|
return s
|
|
}
|
|
|
|
if strings.Contains(varName, " ") {
|
|
mc.logger.InfoContext(ctx, "found white space in var name, replacing it", "dashboard_var_name", varName)
|
|
varName = strings.ReplaceAll(varName, " ", "")
|
|
}
|
|
|
|
return "$" + varName
|
|
}
|