grafana_gen/panels_vl.go

551 lines
21 KiB
Go
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"crypto/md5"
"fmt"
"strings"
)
// queryModeRegistryVL — реестр builders для VictoriaLogs datasource.
// Каждый builder возвращает []interface{} targets в формате VL datasource.
var queryModeRegistryVL = map[string]targetBuilder{
"rps": buildVLRPSTargets,
"rps_var": buildVLRPSVarTargets,
"rps_multi": buildVLRPSMultiTargets,
"multi_target": buildVLMultiTargets,
"range_percent": buildVLRangePercentTargets,
"multi_sum_expression": buildVLMultiSumExpressionTargets,
"sum_expression": buildVLSumExpressionTargets,
"bucket_logs": buildVLBucketLogsTargets,
"static": buildStaticTargets, // переиспользуем — не зависит от datasource
}
// makeVLTarget создаёт target для VictoriaLogs datasource.
// queryType: "range" — для запросов с группировкой by (_time:$__interval, field),
// "statsRange" — для stats-запросов без группировки по времени (возвращает числа напрямую).
func makeVLTarget(refID, expr, datasourceUID, legendFormat string) map[string]interface{} {
return makeVLTargetWithType(refID, expr, datasourceUID, legendFormat, "range")
}
// makeVLStatsTarget создаёт target с queryType="statsRange" для stats-запросов
// без группировки по времени. Плагин VictoriaLogs возвращает числовые данные напрямую.
func makeVLStatsTarget(refID, expr, datasourceUID, legendFormat string) map[string]interface{} {
return makeVLTargetWithType(refID, expr, datasourceUID, legendFormat, "statsRange")
}
// makeVLHiddenStatsTarget создаёт скрытый statsRange target — для использования только в math expressions.
func makeVLHiddenStatsTarget(refID, expr, datasourceUID string) map[string]interface{} {
t := makeVLTargetWithType(refID, expr, datasourceUID, "", "statsRange")
t["hide"] = true
return t
}
func makeVLTargetWithType(refID, expr, datasourceUID, legendFormat, queryType string) map[string]interface{} {
return map[string]interface{}{
"refId": refID,
"expr": expr,
"queryType": queryType,
"editorMode": "code",
"legendFormat": legendFormat,
"maxLines": 1000,
"datasource": map[string]interface{}{
"type": DefaultVLDatasourceType,
"uid": datasourceUID,
},
}
}
// luceneToLogsQL конвертирует базовый Lucene-запрос в LogsQL.
// Поддерживает: AND→пробел, SID:(a OR b)→SID:in(a,b), field:[X TO Y]→field:>=X AND field:<=Y
func luceneToLogsQL(q string) string {
// Убираем Lucene-экранирование — заменяем все \X на X
q = unescapeLucene(q)
// AND → пробел (LogsQL использует пробел как AND)
q = strings.ReplaceAll(q, " AND ", " ")
// SID:(a OR b OR c) → SID:in(a, b, c)
q = convertORGroupToIn(q, "SID")
// SID:({sids}) останется как SID:({sids}) — заменим при построении
// field:[X TO Y] → field:>=X AND field:<=Y
q = convertRangeFilters(q)
return q
}
// convertORGroupToIn конвертирует field:(a OR b OR c) → field:in(a, b, c)
func convertORGroupToIn(q, field string) string {
prefix := field + ":("
start := strings.Index(q, prefix)
if start == -1 {
return q
}
end := strings.Index(q[start:], ")")
if end == -1 {
return q
}
end += start
inner := q[start+len(prefix) : end]
// Заменяем OR на запятые
parts := strings.Split(inner, " OR ")
for i, p := range parts {
p = strings.TrimSpace(p)
// Убираем лишние скобки и кавычки которые Grafana добавляет для single-value переменных
p = strings.Trim(p, "()")
p = strings.Trim(p, `"'`)
parts[i] = strings.TrimSpace(p)
}
newFilter := fmt.Sprintf("%s:in(%s)", field, strings.Join(parts, ", "))
return q[:start] + newFilter + q[end+1:]
}
// convertRangeFilters конвертирует field:[X TO Y] → field:>=X AND field:<=Y
// field:[X TO *] → field:>=X
func convertRangeFilters(q string) string {
// Простая обработка Lucene range syntax
for {
startIdx := strings.Index(q, ":[")
if startIdx == -1 {
break
}
endIdx := strings.Index(q[startIdx:], "]")
if endIdx == -1 {
break
}
endIdx += startIdx
fieldStart := strings.LastIndex(q[:startIdx], " ")
if fieldStart == -1 {
fieldStart = 0
} else {
fieldStart++
}
field := q[fieldStart:startIdx]
inner := q[startIdx+2 : endIdx]
parts := strings.Split(inner, " TO ")
if len(parts) != 2 {
break
}
from := strings.TrimSpace(parts[0])
to := strings.TrimSpace(parts[1])
var newFilter string
if to == "*" {
newFilter = fmt.Sprintf("%s:>=%s", field, from)
} else if from == "*" {
newFilter = fmt.Sprintf("%s:<=%s", field, to)
} else {
newFilter = fmt.Sprintf("%s:>=%s %s:<=%s", field, from, field, to)
}
q = q[:fieldStart] + newFilter + q[endIdx+1:]
}
return q
}
// buildSIDQueryVL строит LogsQL фильтр по SID.
// SID:12345 (один) или SID:in(12345, 67890) (несколько)
func buildSIDQueryVL(sids []string) string {
if len(sids) == 0 {
return "*"
}
if len(sids) == 1 {
return fmt.Sprintf("SID:%s", sids[0])
}
return fmt.Sprintf("SID:in(%s)", strings.Join(sids, ", "))
}
// buildTenantSIDQueryVL строит LogsQL фильтр по всем SID тенанта.
func buildTenantSIDQueryVL(domains []DomainInfo) string {
sids := make([]string, len(domains))
for i, d := range domains {
sids[i] = d.SID
}
return buildSIDQueryVL(sids)
}
// replaceVLPlaceholders заменяет {sids}, {sid}, {client_title} в LogsQL запросе.
// sidFilter может быть как "SID:in(...)" так и голыми SID-значениями.
// Если base_query содержит "SID:{sids}", передаём только значения без префикса SID:.
func replaceVLPlaceholders(q, sidFilter, clientTitle string) string {
// Убираем префикс SID: если он есть — base_query сам содержит SID:
sidValue := sidFilter
if strings.HasPrefix(sidValue, "SID:") {
sidValue = sidValue[4:]
}
// Если base_query содержит SID:{sids} — подставляем без префикса
if strings.Contains(q, "SID:{sids}") || strings.Contains(q, "SID:{sid}") {
q = strings.ReplaceAll(q, "{sids}", sidValue)
q = strings.ReplaceAll(q, "{sid}", sidValue)
} else {
// base_query не содержит SID: — подставляем полный фильтр
q = strings.ReplaceAll(q, "{sids}", sidFilter)
q = strings.ReplaceAll(q, "{sid}", sidFilter)
}
q = strings.ReplaceAll(q, "{client_title}", clientTitle)
return q
}
// ---------- RPS builders ----------
func buildVLRPSTargets(cfg map[string]interface{}, sidQuery string, _ string, datasourceUID string, _ *DashboardTemplate, _ DomainInfo, client ClientData) []interface{} {
vlSID := buildTenantSIDQueryVL(client.Domains)
alias := getString(cfg, "alias")
alias = strings.ReplaceAll(alias, "{client_title}", client.ClientTitle)
if alias == "" {
alias = client.ClientTitle
}
expr := fmt.Sprintf("%s log_type:nginx-access-PTAF | stats by (_time:$__interval) count() rps", vlSID)
return []interface{}{
makeVLHiddenStatsTarget("A", expr, datasourceUID),
makeMathTargetWithAlias("$A / $__interval_ms * 1000", alias, alias),
}
}
func buildVLRPSVarTargets(cfg map[string]interface{}, _ string, _ string, datasourceUID string, _ *DashboardTemplate, _ DomainInfo, client ClientData) []interface{} {
alias := getString(cfg, "alias")
alias = strings.ReplaceAll(alias, "{client_title}", client.ClientTitle)
if alias == "" {
alias = "rps"
}
expr := "SID:${rps_sid} log_type:nginx-access-PTAF | stats by (_time:$__interval) count() rps"
return []interface{}{
makeVLHiddenStatsTarget("A", expr, datasourceUID),
makeMathTargetWithAlias("$A / $__interval_ms * 1000", alias, alias),
}
}
func buildVLRPSMultiTargets(cfg map[string]interface{}, _ string, _ string, datasourceUID string, _ *DashboardTemplate, _ DomainInfo, client ClientData) []interface{} {
targets := []interface{}{}
// Суммарная линия
allSID := buildTenantSIDQueryVL(client.Domains)
totalExpr := fmt.Sprintf("%s log_type:nginx-access-PTAF | stats by (_time:$__interval) count() rps", allSID)
targets = append(targets, makeVLHiddenStatsTarget("A", totalExpr, datasourceUID))
targets = append(targets, makeMathTargetWithAlias("$A / $__interval_ms * 1000", "Total", "Total"))
// Линия на каждый домен
for i, domain := range client.Domains {
rawRefID := fmt.Sprintf("D%d", i)
domainExpr := fmt.Sprintf("SID:%s log_type:nginx-access-PTAF | stats by (_time:$__interval) count() rps", domain.SID)
legend := domain.SID + "_" + domain.DomainName
if domain.DomainName == "" {
legend = domain.SID
}
mathRefID := strings.ReplaceAll(legend, "-", "_")
mathRefID = strings.ReplaceAll(mathRefID, ".", "_")
targets = append(targets, makeVLHiddenStatsTarget(rawRefID, domainExpr, datasourceUID))
targets = append(targets, makeMathTargetWithAlias(fmt.Sprintf("$%s / $__interval_ms * 1000", rawRefID), mathRefID, legend))
}
return targets
}
// ---------- Status codes (multi_target) ----------
func buildVLMultiTargets(cfg map[string]interface{}, sidQuery string, _ string, datasourceUID string, _ *DashboardTemplate, _ DomainInfo, _ ClientData) []interface{} {
targetsRaw, _ := cfg["targets"].([]interface{})
// Уникальный суффикс на основе sidQuery чтобы избежать конфликта refId между панелями
h := md5.Sum([]byte(sidQuery))
uidSuffix := fmt.Sprintf("%x", h[:3])
targets := []interface{}{}
for _, t := range targetsRaw {
tMap, ok := t.(map[string]interface{})
if !ok {
continue
}
refID := getString(tMap, "ref_id") + "_" + uidSuffix
alias := getString(tMap, "alias")
// Если есть vl_base — используем его как базовый запрос вместо sidQuery
vlBase := getString(tMap, "vl_base")
vlSuffixRaw := getString(tMap, "vl_suffix")
var expr string
if vlBase != "" {
// Панель с отдельным base query на каждый target (например nginx vs angie)
if vlSuffixRaw != "" {
expr = fmt.Sprintf("%s %s | stats by (_time:$__interval) count() hits", vlBase, vlSuffixRaw)
} else {
expr = fmt.Sprintf("%s | stats by (_time:$__interval) count() hits", vlBase)
}
} else {
suffix := getString(tMap, "query_suffix")
vlSuffix := luceneToLogsQL(strings.TrimPrefix(suffix, " "))
// Добавляем log_type:nginx-access-PTAF чтобы не смешивать nginx и angie логи
baseWithType := sidQuery + " log_type:nginx-access-PTAF"
if vlSuffix != "" {
expr = fmt.Sprintf("%s %s | stats by (_time:$__interval) count() hits", baseWithType, vlSuffix)
} else {
expr = fmt.Sprintf("%s | stats by (_time:$__interval) count() hits", baseWithType)
}
}
legend := alias
if legend == "" {
legend = refID
}
targets = append(targets, makeVLStatsTarget(refID, expr, datasourceUID, legend))
}
return targets
}
// ---------- Response time (range_percent) ----------
func buildVLRangePercentTargets(cfg map[string]interface{}, sidQuery string, _ string, datasourceUID string, _ *DashboardTemplate, _ DomainInfo, _ ClientData) []interface{} {
baseQuery := luceneToLogsQL(getString(cfg, "base_query"))
baseQuery = replaceVLPlaceholders(baseQuery, sidQuery, "")
rangeField := getString(cfg, "range_field")
rangesRaw, _ := cfg["ranges"].([]interface{})
totalRefID := getString(cfg, "total_ref_id")
if totalRefID == "" {
totalRefID = "F"
}
// Уникальный суффикс чтобы избежать конфликта refId между панелями
h := md5.Sum([]byte(baseQuery))
uidSuffix := fmt.Sprintf("%x", h[:3])
totalRefIDUniq := totalRefID + "_" + uidSuffix
targets := []interface{}{}
// Total target — скрытый, используется только для math
totalExpr := fmt.Sprintf("%s %s:>=0 | stats by (_time:$__interval) count()", baseQuery, rangeField)
targets = append(targets, makeVLHiddenStatsTarget(totalRefIDUniq, totalExpr, datasourceUID))
// Targets для каждого диапазона — скрытые, используются только для math
for _, r := range rangesRaw {
rMap, ok := r.(map[string]interface{})
if !ok {
continue
}
refID := getString(rMap, "ref_id") + "_" + uidSuffix
from := getString(rMap, "from")
to := getString(rMap, "to")
var rangeFilter string
if to == "*" {
rangeFilter = fmt.Sprintf("%s:>=%s", rangeField, from)
} else {
rangeFilter = fmt.Sprintf("%s:>=%s %s:<%s", rangeField, from, rangeField, to)
}
expr := fmt.Sprintf("%s %s | stats by (_time:$__interval) count()", baseQuery, rangeFilter)
targets = append(targets, makeVLHiddenStatsTarget(refID, expr, datasourceUID))
}
// Math targets для процентов — видимые, refId = label (Grafana показывает refId как имя серии)
for _, r := range rangesRaw {
rMap, ok := r.(map[string]interface{})
if !ok {
continue
}
refID := getString(rMap, "ref_id") + "_" + uidSuffix
label := getString(rMap, "label")
if label == "" {
label = refID
}
// Используем label как refId — Grafana показывает refId как имя серии для math targets
safeLabel := strings.ReplaceAll(label, ".", "_")
mathExpr := fmt.Sprintf("$%s / $%s * 100", refID, totalRefIDUniq)
targets = append(targets, makeMathTargetWithAlias(mathExpr, safeLabel, label))
}
return targets
}
// ---------- Traffic (sum_expression) ----------
func buildVLSumExpressionTargets(cfg map[string]interface{}, sidQuery string, _ string, datasourceUID string, _ *DashboardTemplate, _ DomainInfo, _ ClientData) []interface{} {
baseQuery := luceneToLogsQL(getString(cfg, "base_query"))
baseQuery = replaceVLPlaceholders(baseQuery, sidQuery, "")
field := getString(cfg, "field")
refID := getString(cfg, "ref_id")
if refID == "" {
refID = "A"
}
expression := getString(cfg, "expression")
exprRefID := getString(cfg, "expression_ref_id")
// VictoriaLogs: sum по числовому полю, без group by time
expr := fmt.Sprintf("%s | stats by (_time:$__interval) sum(%s) traffic", baseQuery, field)
// Raw target скрытый — используется только для math expression
rawTarget := makeVLHiddenStatsTarget(refID, expr, datasourceUID)
return []interface{}{
rawTarget,
makeMathTargetWithAlias(expression, exprRefID, exprRefID),
}
}
// ---------- Traffic combined (multi_sum_expression) ----------
func buildVLMultiSumExpressionTargets(cfg map[string]interface{}, sidQuery string, _ string, datasourceUID string, _ *DashboardTemplate, _ DomainInfo, client ClientData) []interface{} {
targetsRaw, _ := cfg["targets"].([]interface{})
baseQuery := luceneToLogsQL(getString(cfg, "base_query"))
baseQuery = replaceVLPlaceholders(baseQuery, sidQuery, client.ClientTitle)
targets := []interface{}{}
for _, t := range targetsRaw {
tMap, ok := t.(map[string]interface{})
if !ok {
continue
}
refID := getString(tMap, "ref_id")
expression := getString(tMap, "expression")
alias := getString(tMap, "alias")
if alias == "" {
alias = refID
}
field := getString(tMap, "field")
expr := fmt.Sprintf("%s | stats by (_time:$__interval) sum(%s) traffic", baseQuery, field)
// Raw target скрытый — используется только для math expression
rawTarget := makeVLHiddenStatsTarget(refID, expr, datasourceUID)
// Math target с alias как refId (Grafana показывает refId как имя серии)
targets = append(targets,
rawTarget,
makeMathTargetWithAlias(expression, alias, alias),
)
}
return targets
}
// ---------- Bucket logs (overview panels) ----------
func buildVLBucketLogsTargets(cfg map[string]interface{}, _ string, _ string, datasourceUID string, _ *DashboardTemplate, _ DomainInfo, _ ClientData) []interface{} {
// Если есть vl_base_query — используем его напрямую (уже в LogsQL формате)
// Иначе конвертируем base_query из Lucene
var baseQuery string
if vlRaw := getString(cfg, "vl_base_query"); vlRaw != "" {
// Убираем фильтры вида field:* — они ломают группировку в VL
baseQuery = removeWildcardFilters(vlRaw)
} else {
raw := getString(cfg, "base_query")
raw = strings.ReplaceAll(raw, "SID:(${SID:lucene})", "SID:in(${SID:csv})")
raw = strings.ReplaceAll(raw, "app_name:${app_name:lucene}", "app_name:${app_name}")
raw = strings.ReplaceAll(raw, "response_status_code:${response_status_code:lucene}", "response_status_code:${response_status_code}")
baseQuery = luceneToLogsQL(raw)
}
bucketAggsRaw, _ := cfg["bucket_aggs"].([]interface{})
// Разбираем bucket_aggs чтобы понять по какому полю группировать и top N
var termsField string
var termsSize int
for _, a := range bucketAggsRaw {
aMap, ok := a.(map[string]interface{})
if !ok {
continue
}
if aMap["type"] == "terms" {
termsField = getString(aMap, "field")
// Убираем .keyword суффикс для LogsQL
termsField = strings.TrimSuffix(termsField, ".keyword")
settings, _ := aMap["settings"].(map[string]interface{})
if settings != nil {
if sizeStr, ok := settings["size"].(string); ok && sizeStr != "0" && sizeStr != "" {
fmt.Sscanf(sizeStr, "%d", &termsSize)
}
}
}
}
// Также поддерживаем прямое указание terms_field в query_config
if tf := getString(cfg, "terms_field"); tf != "" {
termsField = tf
}
if termsField == "" {
termsField = "_msg"
}
// vl_metric: "rps" — делим count на интервал для получения RPS
vlMetric := getString(cfg, "vl_metric")
vlInterval := getString(cfg, "vl_interval")
if vlInterval == "" {
vlInterval = "1m"
}
// LogsQL: группировка по полю + top N
var expr string
if vlMetric == "rps" {
// RPS: stats by (time:interval, field) count() / interval_seconds
if termsSize > 0 {
expr = fmt.Sprintf("%s | stats by (_time:%s, %s) count() hits | sort by (hits desc) | limit %d", baseQuery, vlInterval, termsField, termsSize)
} else {
expr = fmt.Sprintf("%s | stats by (_time:%s, %s) count() hits", baseQuery, vlInterval, termsField)
}
} else {
if termsSize > 0 {
expr = fmt.Sprintf("%s | stats by (%s) count() hits | sort by (hits desc) | limit %d", baseQuery, termsField, termsSize)
} else {
expr = fmt.Sprintf("%s | stats by (%s) count() hits", baseQuery, termsField)
}
}
// legendFormat с именем поля — плагин подставит значение поля группировки
legendFormat := fmt.Sprintf("{{%s}}", termsField)
targets := []interface{}{
makeVLStatsTarget("A", expr, datasourceUID, legendFormat),
}
// Для RPS добавляем math expression $A / 60 (интервал 1m = 60 секунд)
if vlMetric == "rps" {
// Скрываем raw query A, показываем только RPS
// legendFormat задаём на raw target — math expression наследует метки
rawTarget := makeVLStatsTarget("A", expr, datasourceUID, legendFormat)
rawTarget["hide"] = true
mathExpr := map[string]interface{}{
"refId": "RPS",
"datasource": map[string]interface{}{"type": "__expr__", "uid": "__expr__"},
"type": "math",
"expression": "$A / 60",
}
targets = []interface{}{
rawTarget,
mathExpr,
}
}
// Если задан unknown_label — добавляем отдельный target для записей с пустым значением поля
unknownLabel := getString(cfg, "unknown_label")
if unknownLabel != "" {
unknownQuery := fmt.Sprintf("%s %s:=\"\" | stats count() hits", baseQuery, termsField)
targets = append(targets, makeVLStatsTarget("B", unknownQuery, datasourceUID, unknownLabel))
}
return targets
}
// unescapeLucene убирает Lucene-экранирование символов вида \X → X
func unescapeLucene(q string) string {
var result strings.Builder
i := 0
for i < len(q) {
if q[i] == '\\' && i+1 < len(q) {
// Пропускаем обратный слеш, берём следующий символ как есть
result.WriteByte(q[i+1])
i += 2
} else {
result.WriteByte(q[i])
i++
}
}
return result.String()
}
// removeWildcardFilters убирает из LogsQL запроса фильтры вида field:* и field:"*"
// которые ломают группировку — VL возвращает один фрейм вместо разбивки по полю.
func removeWildcardFilters(q string) string {
words := strings.Fields(q)
result := []string{}
for _, w := range words {
// Пропускаем field:* и field:"*"
if strings.HasSuffix(w, ":*") || strings.HasSuffix(w, `:"*"`) {
continue
}
result = append(result, w)
}
return strings.Join(result, " ")
}