Files
grafana-zabbix/pkg/zabbix/utils.go
Jocelyn Collado-Kuri 0d64736e86 Adds support for host tags (#2140)
## Sumary
When dealing with multiple hosts, it can be hard for customers filter
through and figure out which host to query metric data from. This PR
aims to make this easier by adding support for host tags so that there
is another layer of filtering / grouping applied for hosts.

## Detailed explanation
- Adds new UI components to allow adding one or more host tag filter,
and a switch to choose between `AND/OR` and `OR` operators when using
more than one filter following Zabbix's UI:
  

https://github.com/user-attachments/assets/c971f5eb-7e93-4238-bd6b-902cc657c014


https://github.com/user-attachments/assets/5f8996de-684e-4ffa-b98e-8e205c4fc1df

- Modifies the existing `getHosts` function to make a call to the
backend with a few additional parameters to `extend` (essentially
extract) the host tags for a given selected group. No backend changes
were required for this.

## Why
To make it easier for customers to query metric data when dealing with
multiple hosts.

## How to test
- Go to explore or a dashboard and create a Zabbix query where the query
type is `Metrics`
- The easiest way to test is by selecting `/.*/` for Groups, checking
the returned `Hosts` they should all be there
- Add a host tag filter and change the keys and operators as well as
switching from `AND/OR` to `OR` you should see how the values returned
for `Host` changes

## Future work
Adding variable support for host tags once this is completed.

Fixes:
https://github.com/orgs/grafana/projects/457/views/40?pane=issue&itemId=3609900134&issue=grafana%7Coss-big-tent-squad%7C126
and https://github.com/grafana/grafana-zabbix/issues/927

---------

Co-authored-by: ismail simsek <ismailsimsek09@gmail.com>
2026-01-05 05:30:55 -08:00

156 lines
3.5 KiB
Go

package zabbix
import (
"fmt"
"regexp"
"strings"
"time"
"github.com/dlclark/regexp2"
"github.com/grafana/grafana-plugin-sdk-go/backend"
)
func (item *Item) ExpandItemName() string {
name := item.Name
key := item.Key
if !strings.Contains(key, "[") {
return name
}
keyParamsStr := key[strings.Index(key, "[")+1 : strings.LastIndex(key, "]")]
keyParams := splitKeyParams(keyParamsStr)
for i := len(keyParams); i >= 1; i-- {
name = strings.ReplaceAll(name, fmt.Sprintf("$%v", i), keyParams[i-1])
}
return name
}
func expandItems(items []*Item) []*Item {
for i := 0; i < len(items); i++ {
items[i].Name = items[i].ExpandItemName()
}
return items
}
func splitKeyParams(paramStr string) []string {
params := []string{}
quoted := false
inArray := false
splitSymbol := ","
param := ""
for _, r := range paramStr {
symbol := string(r)
if symbol == `"` && inArray {
param += symbol
} else if symbol == `"` && quoted {
quoted = false
} else if symbol == `"` && !quoted {
quoted = true
} else if symbol == "[" && !quoted {
inArray = true
} else if symbol == "]" && !quoted {
inArray = false
} else if symbol == splitSymbol && !quoted && !inArray {
params = append(params, param)
param = ""
} else {
param += symbol
}
}
params = append(params, param)
return params
}
// safeRegexpCompile compiles a regex with timeout protection
func safeRegexpCompile(pattern string) (*regexp2.Regexp, error) {
// Channel to receive compilation result
resultCh := make(chan struct {
regex *regexp2.Regexp
err error
}, 1)
// Compile regex in goroutine with timeout
go func() {
regex, err := regexp2.Compile(pattern, regexp2.RE2)
resultCh <- struct {
regex *regexp2.Regexp
err error
}{regex, err}
}()
// Wait for compilation or timeout
select {
case result := <-resultCh:
return result.regex, result.err
case <-time.After(5 * time.Second):
return nil, fmt.Errorf("regex compilation timeout (5s) - pattern may be too complex")
}
}
func parseFilter(filter string) (*regexp2.Regexp, error) {
vaildREModifiers := "imncsxrde"
regex := regexp.MustCompile(`^/(.+)/([imncsxrde]*)$`)
flagRE := regexp.MustCompile(fmt.Sprintf("[%s]+", vaildREModifiers))
matches := regex.FindStringSubmatch(filter)
if len(matches) <= 1 {
return nil, nil
}
regexPattern := matches[1]
pattern := ""
if matches[2] != "" {
if flagRE.MatchString(matches[2]) {
pattern += "(?" + matches[2] + ")"
} else {
return nil, backend.DownstreamErrorf("error parsing regexp: unsupported flags `%s` (expected [%s])", matches[2], vaildREModifiers)
}
}
pattern += regexPattern
// Security: Compile regex with timeout protection
compiled, err := safeRegexpCompile(pattern)
if err != nil {
return nil, backend.DownstreamErrorf("error parsing regexp: %v", err)
}
// Set match timeout for runtime DoS protection (5 second timeout)
compiled.MatchTimeout = 5 * time.Second
return compiled, nil
}
func isRegex(filter string) bool {
regex := regexp.MustCompile(`^/(.+)/([imncsxrde]*)$`)
return regex.MatchString(filter)
}
func itemTagToString(tag Tag) string {
if tag.Value != "" {
return fmt.Sprintf("%s: %s", tag.Tag, tag.Value)
} else {
return tag.Tag
}
}
func parseItemTag(tagStr string) Tag {
tag := Tag{}
firstIdx := strings.Index(tagStr, ":")
if firstIdx > 0 {
tag.Tag = strings.TrimSpace(tagStr[:firstIdx])
if firstIdx < len(tagStr)-1 {
tag.Value = strings.TrimSpace(tagStr[firstIdx+1:])
}
} else {
tag.Tag = strings.TrimSpace(tagStr)
}
return tag
}