@@ -18,19 +18,16 @@ const (
1818 defaultRefreshSec = 60
1919 rulesRequestTimeout = 10 * time .Second
2020 activeRulesPath = "/api/v1/internal/alert-tag-rules/active"
21+ enrichmentPath = "/api/v1/datasources/enrichment"
2122)
2223
23- // RuleSnapshot is the per-rule view the plugin needs at evaluation time.
24- // Conditions is left as raw JSON until used so we don't decode work we won't
25- // run (a rule with empty conditions never reaches the evaluator).
2624type RuleSnapshot struct {
2725 ID uint64
2826 Name string
2927 Conditions []FilterType
3028 TagNames []string
3129}
3230
33- // activeRuleWire matches the backend dto.ActiveAlertTagRule payload.
3431type activeRuleWire struct {
3532 ID uint64 `json:"id"`
3633 Name string `json:"name"`
@@ -161,3 +158,128 @@ func (c *ruleCache) Run(ctx context.Context) {
161158 }
162159 }
163160}
161+
162+ type datasourceEnrichment struct {
163+ GroupID * uint64
164+ GroupName string
165+ Labels []string
166+ }
167+
168+ type enrichmentWire struct {
169+ Name string `json:"name"`
170+ DataType string `json:"dataType"`
171+ GroupID * uint64 `json:"groupId"`
172+ GroupName string `json:"groupName"`
173+ Labels []string `json:"labels"`
174+ }
175+
176+ type datasourceCache struct {
177+ baseURL string
178+ internalKey string
179+ httpClient * http.Client
180+ refresh time.Duration
181+
182+ mu sync.RWMutex
183+ byName map [string ]datasourceEnrichment
184+ }
185+
186+ func newDatasourceCache () * datasourceCache {
187+ cfg := plugins .PluginCfg ("com.utmstack" )
188+ base := cfg .Get ("backend" ).String ()
189+ if base != "" && ! strings .HasPrefix (base , "http://" ) && ! strings .HasPrefix (base , "https://" ) {
190+ base = "http://" + base
191+ }
192+ refresh := time .Duration (defaultRefreshSec ) * time .Second
193+ if v := cfg .Get ("rulesRefreshSec" ).Int (); v > 0 {
194+ refresh = time .Duration (v ) * time .Second
195+ }
196+ return & datasourceCache {
197+ baseURL : base ,
198+ internalKey : cfg .Get ("internalKey" ).String (),
199+ httpClient : & http.Client {Timeout : rulesRequestTimeout },
200+ refresh : refresh ,
201+ }
202+ }
203+
204+ func (c * datasourceCache ) Lookup (name string ) (datasourceEnrichment , bool ) {
205+ c .mu .RLock ()
206+ defer c .mu .RUnlock ()
207+ e , ok := c .byName [name ]
208+ return e , ok
209+ }
210+
211+ func (c * datasourceCache ) Refresh (ctx context.Context ) error {
212+ if c .baseURL == "" || c .internalKey == "" {
213+ return catcher .Error ("datasource cache: backend URL or internal key missing" , nil , map [string ]any {"process" : "plugin_com.utmstack.alerts" })
214+ }
215+
216+ req , err := http .NewRequestWithContext (ctx , http .MethodGet , c .baseURL + enrichmentPath , nil )
217+ if err != nil {
218+ return catcher .Error ("datasource cache: build request failed" , err , map [string ]any {"process" : "plugin_com.utmstack.alerts" })
219+ }
220+ req .Header .Set (internalKeyHeader , c .internalKey )
221+
222+ resp , err := c .httpClient .Do (req )
223+ if err != nil {
224+ return catcher .Error ("datasource cache: request failed" , err , map [string ]any {"process" : "plugin_com.utmstack.alerts" })
225+ }
226+ defer func () { _ = resp .Body .Close () }()
227+
228+ body , err := io .ReadAll (resp .Body )
229+ if err != nil {
230+ return catcher .Error ("datasource cache: read body failed" , err , map [string ]any {"process" : "plugin_com.utmstack.alerts" })
231+ }
232+ if resp .StatusCode >= 400 {
233+ return catcher .Error ("datasource cache: backend returned error" , nil , map [string ]any {
234+ "status" : resp .StatusCode ,
235+ "body" : string (body ),
236+ "process" : "plugin_com.utmstack.alerts" ,
237+ })
238+ }
239+
240+ var wire []enrichmentWire
241+ if err := json .Unmarshal (body , & wire ); err != nil {
242+ return catcher .Error ("datasource cache: decode failed" , err , map [string ]any {"process" : "plugin_com.utmstack.alerts" })
243+ }
244+
245+ next := make (map [string ]datasourceEnrichment , len (wire ))
246+ for _ , w := range wire {
247+ // Keyed by name; a host's group is the same across its data types, so a
248+ // repeated name just overwrites with equivalent enrichment.
249+ next [w .Name ] = datasourceEnrichment {
250+ GroupID : w .GroupID ,
251+ GroupName : w .GroupName ,
252+ Labels : w .Labels ,
253+ }
254+ }
255+
256+ c .mu .Lock ()
257+ c .byName = next
258+ c .mu .Unlock ()
259+ return nil
260+ }
261+
262+ func (c * datasourceCache ) Run (ctx context.Context ) {
263+ defer func () {
264+ if r := recover (); r != nil {
265+ _ = catcher .Error ("datasource cache: recovered from panic in Run" , nil , map [string ]any {
266+ "panic" : r ,
267+ "process" : "plugin_com.utmstack.alerts" ,
268+ })
269+ }
270+ }()
271+
272+ ticker := time .NewTicker (c .refresh )
273+ defer ticker .Stop ()
274+
275+ for {
276+ select {
277+ case <- ctx .Done ():
278+ return
279+ case <- ticker .C :
280+ refreshCtx , cancel := context .WithTimeout (context .Background (), rulesRequestTimeout )
281+ _ = c .Refresh (refreshCtx )
282+ cancel ()
283+ }
284+ }
285+ }
0 commit comments