Files
4102219754 fork: own SigNoz platform deps under hanzoai + repath off upstream brand (Phase 1) (#9)
* fork: own SigNoz platform deps under hanzoai, repath imports off upstream brand

Phase 1 of the full-fork mandate: eliminate upstream-branded SigNoz modules,
own them as public hanzoai forks, and repath all imports to hanzoai paths.

Owned deps (new public hanzoai forks, module renamed to hanzoai path):
  github.com/SigNoz/signoz-otel-collector v0.144.3
    -> github.com/hanzoai/signoz-otel-collector v0.144.6 (module renamed)
  github.com/SigNoz/clickhouse-go-mock v0.14.0
    -> github.com/hanzoai/clickhouse-go-mock v0.14.1 (module renamed)
  github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
    -> github.com/hanzoai/govaluate v0.1.0 (module renamed)
  github.com/SigNoz/expr (replace target for github.com/expr-lang/expr)
    -> github.com/hanzoai/expr v1.17.8 (upstream module path preserved,
       consumed via replace exactly like the prior SigNoz/expr fork)

In-repo repath: the vendored SigNoz source tree in pkg/, ee/, cmd/ is now
self-referenced as github.com/hanzoai/o11y/... instead of
github.com/SigNoz/signoz/... (1300+ import lines across 377 files), matching
the 750 files already on the hanzoai path. Zero SigNoz-branded module
references remain in go.mod or any .go import.

Generic ecosystem libraries (golang.org/x, google.golang.org, prometheus,
opentelemetry, gin, cobra, luxfi/*, etc.) are left untouched — only the
SigNoz platform fork is owned.

Pre-existing (NOT introduced here): origin/main does not `go build ./...`
because 22 packages are imported but absent from the vendored tree; 14 of
them (roletypes, grammar, cloudintegrations, resourcefilter, integrationtypes,
metricsexplorer, metrics_explorer, times, agg_rewrite, bucket_cache,
sqlitesqlstore, sqlschema/postgressqlschema, sqlstore/postgressqlstore,
cloudintegrations/services) exist in no upstream SigNoz version and must be
vendored into hanzoai/o11y from hanzoai/signoz@770f464f to finish. This
repath is orthogonal to that gap.

* docs(LLM.md): fork boundary map + pre-existing compile-gap inventory

---------

Co-authored-by: Hanzo Dev <[email protected]>
2026-06-30 18:05:03 -07:00

156 lines
5.6 KiB
Go

package alertmanagertemplate
import (
"fmt"
"reflect"
"regexp"
"strings"
"text/template/parse"
"github.com/hanzoai/o11y/pkg/errors"
)
// bareVariableRegex matches $-references including dotted paths (e.g. $alert.is_firing).
var bareVariableRegex = regexp.MustCompile(`\$(\w+(?:\.\w+)*)`)
// bareVariableFirstSegRegex captures only the first segment of a $-reference.
// $labels.severity yields "$labels"; $alert.is_firing yields "$alert".
var bareVariableFirstSegRegex = regexp.MustCompile(`\$\w+`)
// wrapDollarVariables rewrites bare $-references in a template's plain-text
// regions into Go text/template syntax. References inside `{{ ... }}` action
// blocks are left untouched — they're already template syntax. structRoots
// is the set of top-level mapstructure names whose values are nested structs
// (e.g. "alert", "rule"): $-refs whose first segment is in this set are
// walked segment-by-segment; everything else dotted is treated as a flat key
// on the root map so flattened OTel-style label keys resolve naturally.
//
// Examples (structRoots = {alert, rule}):
//
// "$name is $status" -> "{{ .name }} is {{ .status }}"
// "$labels.severity" -> `{{ index .labels "severity" }}`
// "$labels.http.method" -> `{{ index .labels "http.method" }}`
// "$annotations.summary" -> `{{ index .annotations "summary" }}`
// "$alert.is_firing" -> `{{ index . "alert" "is_firing" }}`
// "$rule.threshold.value" -> `{{ index . "rule" "threshold" "value" }}`
// "$service.name" -> `{{ index . "service.name" }}`
// "$name is {{ .Status }}" -> "{{ .name }} is {{ .Status }}"
func wrapDollarVariables(src string, structRoots map[string]bool) (string, error) {
if src == "" {
return src, nil
}
tree := parse.New("template")
tree.Mode = parse.SkipFuncCheck
if _, err := tree.Parse(src, "{{", "}}", make(map[string]*parse.Tree), nil); err != nil {
return "", err
}
walkAndWrapTextNodes(tree.Root, structRoots)
return tree.Root.String(), nil
}
// walkAndWrapTextNodes descends the parse tree and rewrites $-references
// found in TextNodes. If/Range bodies are recursed into. ActionNodes and
// other `{{...}}` constructs are left alone because they're already template
// syntax. WithNode is not yet supported — add when the editor grows a "with"
// block.
func walkAndWrapTextNodes(node parse.Node, structRoots map[string]bool) {
if reflect.ValueOf(node).IsNil() {
return
}
switch n := node.(type) {
case *parse.ListNode:
if n.Nodes != nil {
for _, child := range n.Nodes {
walkAndWrapTextNodes(child, structRoots)
}
}
case *parse.TextNode:
n.Text = bareVariableRegex.ReplaceAllFunc(n.Text, func(match []byte) []byte {
return rewriteDollarRef(match, structRoots)
})
case *parse.IfNode:
walkAndWrapTextNodes(n.List, structRoots)
walkAndWrapTextNodes(n.ElseList, structRoots)
case *parse.RangeNode:
walkAndWrapTextNodes(n.List, structRoots)
walkAndWrapTextNodes(n.ElseList, structRoots)
}
}
// rewriteDollarRef converts one $-reference (with the leading $) into the
// corresponding Go template expression.
//
// - labels./annotations. prefixes: treat the remainder as a single map key
// (OTel-style keys like "http.request.method" are flat keys, not paths).
// - Dotted path with a struct-root first segment: walk via chained index
// arguments. `index x a b c` is equivalent to x[a][b][c].
// - Other dotted path: treat as a single flat key on the root map, so a
// flattened OTel-style label key like "service.name" resolves.
// - Simple names: plain dot access on the root map.
func rewriteDollarRef(match []byte, structRoots map[string]bool) []byte {
name := string(match[1:])
if !strings.Contains(name, ".") {
return fmt.Appendf(nil, `{{ .%s }}`, name)
}
if key, ok := strings.CutPrefix(name, "labels."); ok {
return fmt.Appendf(nil, `{{ index .labels %q }}`, key)
}
if key, ok := strings.CutPrefix(name, "annotations."); ok {
return fmt.Appendf(nil, `{{ index .annotations %q }}`, key)
}
// If the first segment is a known struct root, walk segments.
if dot := strings.IndexByte(name, '.'); dot >= 0 && structRoots[name[:dot]] {
parts := strings.Split(name, ".")
args := make([]string, len(parts))
for i, p := range parts {
args[i] = fmt.Sprintf("%q", p)
}
return fmt.Appendf(nil, `{{ index . %s }}`, strings.Join(args, " "))
}
// Otherwise treat the full dotted path as a single flat root key.
return fmt.Appendf(nil, `{{ index . %q }}`, name)
}
// extractUsedVariables returns the set of every base $-name referenced in the
// template — text nodes, action blocks, branch conditions, loop declarations.
// Names are first-segment only: $labels.severity contributes "labels".
//
// The template is validated during extraction (a synthesised preamble
// pre-declares each name so the parser doesn't trip on "undefined variable"
// for names that genuinely exist in the data), so a parse error here
// indicates a genuine syntax problem rather than a missing binding.
func extractUsedVariables(src string) (map[string]bool, error) {
if src == "" {
return map[string]bool{}, nil
}
used := make(map[string]bool)
for _, m := range bareVariableFirstSegRegex.FindAll([]byte(src), -1) {
used[string(m[1:])] = true
}
var preamble strings.Builder
for name := range used {
fmt.Fprintf(&preamble, `{{$%s := ""}}`, name)
}
tree := parse.New("template")
tree.Mode = parse.SkipFuncCheck
if _, err := tree.Parse(preamble.String()+src, "{{", "}}", make(map[string]*parse.Tree), nil); err != nil {
return nil, errors.WrapInvalidInputf(err, errors.CodeInternal, "failed to extract used variables")
}
return used, nil
}