mirror of
https://github.com/grafana/grafana.git
synced 2025-08-03 03:13:49 +08:00

* Plugins: Angular detector: Remote patterns fetching * Renamed PatternType to GCOMPatternType * Renamed files * Renamed more files * Moved files again * Add type checks, unexport GCOM structs * Cache failures, update log messages, fix GCOM URL * Fail silently for unknown pattern types, update docstrings * Fix tests * Rename gcomPattern.Value to gcomPattern.Pattern * Refactoring * Add FlagPluginsRemoteAngularDetectionPatterns feature flag * Fix tests * Re-generate feature flags * Add TestProvideInspector, renamed TestDefaultStaticDetectorsInspector * Add TestProvideInspector * Add TestContainsBytesDetector and TestRegexDetector * Renamed getter to provider * More tests * TestStaticDetectorsProvider, TestSequenceDetectorsProvider * GCOM tests * Lint * Made detector.detect unexported, updated docstrings * Allow changing grafana.com URL * Fix API path, add more logs * Update tryUpdateRemoteDetectors docstring * Use angulardetector http client * Return false, nil if module.js does not exist * Chore: Split angualrdetector into angularinspector and angulardetector packages Moved files around, changed references and fixed tests: - Split the old angulardetector package into angular/angulardetector and angular/angularinspector - angulardetector provides the detection structs/interfaces (Detector, DetectorsProvider...) - angularinspector provides the actual angular detection service used directly in pluginsintegration - Exported most of the stuff that was private and now put into angulardetector, as it is not required by angularinspector * Renamed detector.go -> angulardetector.go and inspector.go -> angularinspector.go Forgot to rename those two files to match the package's names * Renamed angularinspector.ProvideInspector to angularinspector.ProvideService * Renamed "harcoded" to "static" and "remote" to "dynamic" from PR review, matches the same naming schema used for signing keys fetching * WIP: Angular: cache patterns in db, moved gcom into pluginsintegration More similar to signing keys fetching * Rename package, refactoring * try to solve circular import * Fix merge conflict on updated angular patterns * Fix circular imports * Fix wire gen * Add docstrings, refactoring * Removed angualrdetectorsprovider dependency into angularpatternsstore * Moved GCOM test files * Removed GCOM cache * Renamed Detect to DetectAngular and Detector to AngularDetector * Fix call to NewGCOMDetectorsProvider in newDynamicInspector * Removed unused test function newError500GCOMScenario * Added angularinspector service definition in pluginsintegration * refactoring * lint * Fix angularinspector TestProvideService * cleanup * Await initial restore * Register dynamicAngularDetector background service * Removed static detectors provider from pluginsintegration * Add tests for kvstore * Add more tests * order imports in dynamic_test.go * Fix potential panic in dynamic_test * Add "runs the job periodically" test * lint * add timeout to test * refactoring * Removed context.Context from DetectorsProvider * Refactoring, ensure angular dynamic background service is not started if feature flag is off * Fix deadlock on startup * Fix angulardetectorsprovider tests * Revert "Removed context.Context from DetectorsProvider" This reverts commit 4e8c6dded70844709400fa0ce4ce45e66c8458ca. * Fix wrong argument number in dynamic_teset * Standardize gcom http client * Reduce context timeout for angular inspector in plugins loader * Simplify initial restore logic * Fix dynamic detectors provider tests * Chore: removed angulardetector/provider.go * Add more tests * Removed backgroundJob interface, PR review feedback * Update tests * PR review feedback: remove ErrNoCachedValue from kv store Get * Update tests * PR review feedback: add IsDisabled and remove nop background srevice * Update tests * Remove initialRestore channel, use mux instead * Removed backgroundJobInterval, use package-level variable instead * Add TestDynamicAngularDetectorsProviderBackgroundService * Removed timeouts * pr review feedback: restore from store before returning the service * Update tests * Log duration on startup restore and cron run * Switch cron job start log to debug level * Do not attempt to restore if disabled
262 lines
8.1 KiB
Go
262 lines
8.1 KiB
Go
package angulardetectorsprovider
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana/pkg/plugins/config"
|
|
"github.com/grafana/grafana/pkg/plugins/log"
|
|
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angulardetector"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/angularpatternsstore"
|
|
)
|
|
|
|
// backgroundJobInterval is the interval that passes between background job runs.
|
|
// It can be overwritten in tests.
|
|
var backgroundJobInterval = time.Hour * 1
|
|
|
|
// Dynamic is an angulardetector.DetectorsProvider that calls GCOM to get Angular detection patterns,
|
|
// converts them to detectors and caches them for all future calls.
|
|
// It also provides a background service that will periodically refresh the patterns from GCOM.
|
|
// If the feature flag FlagPluginsDynamicAngularDetectionPatterns is disabled, the background service is disabled.
|
|
type Dynamic struct {
|
|
log log.Logger
|
|
features featuremgmt.FeatureToggles
|
|
|
|
httpClient http.Client
|
|
baseURL string
|
|
|
|
// store is the underlying angular patterns store used as a cache.
|
|
store angularpatternsstore.Service
|
|
|
|
// detectors contains the cached angular detectors, which are created from the remote angular patterns.
|
|
// mux should be acquired before reading from/writing to this field.
|
|
detectors []angulardetector.AngularDetector
|
|
|
|
// mux is the mutex used to read/write the cached detectors in a concurrency-safe way.
|
|
mux sync.RWMutex
|
|
}
|
|
|
|
func ProvideDynamic(cfg *config.Cfg, store angularpatternsstore.Service, features featuremgmt.FeatureToggles) (*Dynamic, error) {
|
|
d := &Dynamic{
|
|
log: log.New("plugin.angulardetectorsprovider.dynamic"),
|
|
features: features,
|
|
store: store,
|
|
httpClient: makeHttpClient(),
|
|
baseURL: cfg.GrafanaComURL,
|
|
}
|
|
if d.IsDisabled() {
|
|
// Do not attempt to restore if the background service is disabled (no feature flag)
|
|
return d, nil
|
|
}
|
|
|
|
// Perform the initial restore from db
|
|
st := time.Now()
|
|
d.log.Debug("Restoring cache")
|
|
if err := d.setDetectorsFromCache(context.Background()); err != nil {
|
|
d.log.Warn("Cache restore failed", "error", err)
|
|
} else {
|
|
d.log.Info("Restored cache from database", "duration", time.Since(st))
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// patternsToDetectors converts a slice of gcomPattern into a slice of angulardetector.AngularDetector, by calling
|
|
// angularDetector() on each gcomPattern.
|
|
func (d *Dynamic) patternsToDetectors(patterns GCOMPatterns) ([]angulardetector.AngularDetector, error) {
|
|
var finalErr error
|
|
detectors := make([]angulardetector.AngularDetector, 0, len(patterns))
|
|
for _, pattern := range patterns {
|
|
ad, err := pattern.angularDetector()
|
|
if err != nil {
|
|
// Fail silently in case of an errUnknownPatternType.
|
|
// This allows us to introduce new pattern types without breaking old Grafana versions
|
|
if errors.Is(err, errUnknownPatternType) {
|
|
d.log.Debug("Unknown angular pattern", "name", pattern.Name, "type", pattern.Type, "error", err)
|
|
continue
|
|
}
|
|
// Other error, do not ignore it
|
|
finalErr = errors.Join(finalErr, err)
|
|
}
|
|
detectors = append(detectors, ad)
|
|
}
|
|
if finalErr != nil {
|
|
return nil, finalErr
|
|
}
|
|
return detectors, nil
|
|
}
|
|
|
|
// fetch fetches the angular patterns from GCOM and returns them as GCOMPatterns.
|
|
// Call detectors() on the returned value to get the corresponding detectors.
|
|
func (d *Dynamic) fetch(ctx context.Context) (GCOMPatterns, error) {
|
|
st := time.Now()
|
|
|
|
reqURL, err := url.JoinPath(d.baseURL, gcomAngularPatternsPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("url joinpath: %w", err)
|
|
}
|
|
|
|
d.log.Debug("Fetching dynamic angular detection patterns", "url", reqURL)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("new request with context: %w", err)
|
|
}
|
|
resp, err := d.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("http do: %w", err)
|
|
}
|
|
defer func() {
|
|
if closeErr := resp.Body.Close(); closeErr != nil {
|
|
d.log.Error("Response body close error", "error", err)
|
|
}
|
|
}()
|
|
var out GCOMPatterns
|
|
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
|
return nil, fmt.Errorf("json decode: %w", err)
|
|
}
|
|
d.log.Debug("Fetched dynamic angular detection patterns", "patterns", len(out), "duration", time.Since(st))
|
|
return out, nil
|
|
}
|
|
|
|
// updateDetectors fetches the patterns from GCOM, converts them to detectors,
|
|
// stores the patterns in the database and update the cached detectors.
|
|
func (d *Dynamic) updateDetectors(ctx context.Context) error {
|
|
// Fetch patterns from GCOM
|
|
d.mux.Lock()
|
|
defer d.mux.Unlock()
|
|
patterns, err := d.fetch(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("fetch: %w", err)
|
|
}
|
|
|
|
// Convert the patterns to detectors
|
|
newDetectors, err := d.patternsToDetectors(patterns)
|
|
if err != nil {
|
|
return fmt.Errorf("patterns convert to detectors: %w", err)
|
|
}
|
|
|
|
// Update store only if the patterns can be converted to detectors
|
|
if err := d.store.Set(ctx, patterns); err != nil {
|
|
return fmt.Errorf("store set: %w", err)
|
|
}
|
|
|
|
// Update cached detectors
|
|
d.detectors = newDetectors
|
|
return nil
|
|
}
|
|
|
|
// setDetectorsFromCache sets the in-memory detectors from the patterns in the store.
|
|
// The caller must Lock d.mux before calling this function.
|
|
func (d *Dynamic) setDetectorsFromCache(ctx context.Context) error {
|
|
d.mux.Lock()
|
|
defer d.mux.Unlock()
|
|
|
|
var cachedPatterns GCOMPatterns
|
|
rawCached, ok, err := d.store.Get(ctx)
|
|
if !ok {
|
|
// No cached value found, do not alter in-memory detectors
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("get cached value: %w", err)
|
|
}
|
|
// Try to unmarshal, convert to detectors and set local cache
|
|
if err := json.Unmarshal([]byte(rawCached), &cachedPatterns); err != nil {
|
|
return fmt.Errorf("json unmarshal: %w", err)
|
|
}
|
|
cachedDetectors, err := d.patternsToDetectors(cachedPatterns)
|
|
if err != nil {
|
|
return fmt.Errorf("convert to detectors: %w", err)
|
|
}
|
|
d.detectors = cachedDetectors
|
|
return nil
|
|
}
|
|
|
|
// IsDisabled returns true if FlagPluginsDynamicAngularDetectionPatterns is not enabled.
|
|
func (d *Dynamic) IsDisabled() bool {
|
|
return !d.features.IsEnabled(featuremgmt.FlagPluginsDynamicAngularDetectionPatterns)
|
|
}
|
|
|
|
// Run is the function implementing the background service and updates the detectors periodically.
|
|
func (d *Dynamic) Run(ctx context.Context) error {
|
|
d.log.Debug("Started background service")
|
|
|
|
// Determine when next run is, and check if we should run immediately
|
|
lastUpdate, err := d.store.GetLastUpdated(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("get last updated: %w", err)
|
|
}
|
|
nextRunUntil := time.Until(lastUpdate.Add(backgroundJobInterval))
|
|
|
|
ticker := time.NewTicker(backgroundJobInterval)
|
|
defer ticker.Stop()
|
|
|
|
var tick <-chan time.Time
|
|
if nextRunUntil <= 0 {
|
|
// Do first run immediately
|
|
firstTick := make(chan time.Time, 1)
|
|
tick = firstTick
|
|
|
|
firstTick <- time.Now()
|
|
} else {
|
|
// Do first run after a certain amount of time
|
|
ticker.Reset(nextRunUntil)
|
|
tick = ticker.C
|
|
}
|
|
|
|
// Keep running periodically
|
|
for {
|
|
select {
|
|
case <-tick:
|
|
st := time.Now()
|
|
d.log.Debug("Updating patterns")
|
|
|
|
if err := d.updateDetectors(context.Background()); err != nil {
|
|
d.log.Error("Error while updating detectors", "error", err)
|
|
}
|
|
d.log.Info("Patterns update finished", "duration", time.Since(st))
|
|
|
|
// Restore default ticker if we run with a shorter interval the first time
|
|
ticker.Reset(backgroundJobInterval)
|
|
tick = ticker.C
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
// ProvideDetectors returns the cached detectors. It returns an empty slice if there's no value.
|
|
func (d *Dynamic) ProvideDetectors(_ context.Context) []angulardetector.AngularDetector {
|
|
d.mux.RLock()
|
|
r := d.detectors
|
|
d.mux.RUnlock()
|
|
return r
|
|
}
|
|
|
|
// Same configuration as pkg/plugins/repo/client.go
|
|
func makeHttpClient() http.Client {
|
|
tr := &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
DialContext: (&net.Dialer{
|
|
Timeout: 30 * time.Second,
|
|
KeepAlive: 30 * time.Second,
|
|
}).DialContext,
|
|
MaxIdleConns: 100,
|
|
IdleConnTimeout: 90 * time.Second,
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
ExpectContinueTimeout: 1 * time.Second,
|
|
}
|
|
|
|
return http.Client{
|
|
Timeout: 10 * time.Second,
|
|
Transport: tr,
|
|
}
|
|
}
|