mirror of
https://github.com/karust/openserp.git
synced 2026-09-09 04:56:23 +08:00
Improve logging: add structured logs, enrich logs with useful info.
This commit is contained in:
+13
-3
@@ -73,11 +73,16 @@ func (baid *Baidu) isTimeout(page *rod.Page) bool {
|
|||||||
// Search executes a Baidu web search and returns normalized search results.
|
// Search executes a Baidu web search and returns normalized search results.
|
||||||
// It may return core.ErrCaptcha or core.ErrSearchTimeout.
|
// It may return core.ErrCaptcha or core.ErrSearchTimeout.
|
||||||
func (baid *Baidu) Search(ctx context.Context, query core.Query) (results []core.SearchResult, err error) {
|
func (baid *Baidu) Search(ctx context.Context, query core.Query) (results []core.SearchResult, err error) {
|
||||||
ctx = core.EnsureContext(ctx)
|
ctx = core.WithEngine(core.EnsureContext(ctx), baid.Name())
|
||||||
|
ctx = core.WithQueryHash(ctx, core.QueryHashFromQuery(query))
|
||||||
|
scoped := *baid
|
||||||
|
scoped.logger = baid.logger.WithRequest(ctx)
|
||||||
|
baid = &scoped
|
||||||
|
|
||||||
baid.logger.Debug("Starting search, query: %+v", query)
|
baid.logger.Debug("Starting search, query: %+v", query)
|
||||||
defer func() {
|
defer func() {
|
||||||
if recovered := recover(); recovered != nil {
|
if recovered := recover(); recovered != nil {
|
||||||
err = core.RecoverEnginePanic(baid.Name(), recovered, baid.logger)
|
err = core.RecoverEnginePanicWithContext(ctx, baid.Name(), recovered, baid.logger)
|
||||||
results = nil
|
results = nil
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -171,7 +176,12 @@ func (baid *Baidu) Search(ctx context.Context, query core.Query) (results []core
|
|||||||
// SearchImage executes a Baidu image search and returns normalized image
|
// SearchImage executes a Baidu image search and returns normalized image
|
||||||
// results. It may return core.ErrCaptcha or core.ErrSearchTimeout.
|
// results. It may return core.ErrCaptcha or core.ErrSearchTimeout.
|
||||||
func (baid *Baidu) SearchImage(ctx context.Context, query core.Query) ([]core.SearchResult, error) {
|
func (baid *Baidu) SearchImage(ctx context.Context, query core.Query) ([]core.SearchResult, error) {
|
||||||
ctx = core.EnsureContext(ctx)
|
ctx = core.WithEngine(core.EnsureContext(ctx), baid.Name())
|
||||||
|
ctx = core.WithQueryHash(ctx, core.QueryHashFromQuery(query))
|
||||||
|
scoped := *baid
|
||||||
|
scoped.logger = baid.logger.WithRequest(ctx)
|
||||||
|
baid = &scoped
|
||||||
|
|
||||||
baid.logger.Debug("Starting image search, query: %+v", query)
|
baid.logger.Debug("Starting image search, query: %+v", query)
|
||||||
|
|
||||||
searchResults := []core.SearchResult{}
|
searchResults := []core.SearchResult{}
|
||||||
|
|||||||
+14
-5
@@ -2,6 +2,7 @@ package baidu
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -86,15 +87,19 @@ func baiduResultParser(response *http.Response) ([]core.SearchResult, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Tracef("Baidu search document size: %d", len(doc.Text()))
|
logrus.WithField("document_size", len(doc.Text())).Trace(
|
||||||
|
fmt.Sprintf("Baidu search document size: %d", len(doc.Text())),
|
||||||
|
)
|
||||||
return results, err
|
return results, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func Search(ctx context.Context, query core.Query) (results []core.SearchResult, err error) {
|
func Search(ctx context.Context, query core.Query) (results []core.SearchResult, err error) {
|
||||||
ctx = core.EnsureContext(ctx)
|
ctx = core.EnsureContext(ctx)
|
||||||
|
ctx = core.WithEngine(ctx, "baidu")
|
||||||
|
ctx = core.WithQueryHash(ctx, core.QueryHashFromQuery(query))
|
||||||
defer func() {
|
defer func() {
|
||||||
if recovered := recover(); recovered != nil {
|
if recovered := recover(); recovered != nil {
|
||||||
err = core.RecoverEnginePanic("baidu", recovered, nil)
|
err = core.RecoverEnginePanicWithContext(ctx, "baidu", recovered, nil)
|
||||||
results = nil
|
results = nil
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -103,14 +108,16 @@ func Search(ctx context.Context, query core.Query) (results []core.SearchResult,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
logrus.Debugf("Baidu URL built: %s", googleURL)
|
core.WithRequest(ctx).WithField("url", googleURL).Debug(fmt.Sprintf("Baidu URL built: %s", googleURL))
|
||||||
|
|
||||||
res, err := baiduRequest(ctx, googleURL, query)
|
res, err := baiduRequest(ctx, googleURL, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer core.DrainAndCloseResponse(res)
|
defer core.DrainAndCloseResponse(res)
|
||||||
logrus.Debugf("Baidu Raw response: code=%d", res.StatusCode)
|
core.WithRequest(ctx).WithField("status_code", res.StatusCode).Debug(
|
||||||
|
fmt.Sprintf("Baidu Raw response: code=%d", res.StatusCode),
|
||||||
|
)
|
||||||
|
|
||||||
parsedResults, err := baiduResultParser(res)
|
parsedResults, err := baiduResultParser(res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -121,7 +128,9 @@ func Search(ctx context.Context, query core.Query) (results []core.SearchResult,
|
|||||||
parsedResults[i].Rank = query.Start + i + 1
|
parsedResults[i].Rank = query.Start + i + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logrus.Debugf("Baidu Raw results : %v", parsedResults)
|
core.WithRequest(ctx).WithField("results_count", len(parsedResults)).Debug(
|
||||||
|
fmt.Sprintf("Baidu Raw results : %v", parsedResults),
|
||||||
|
)
|
||||||
|
|
||||||
return core.DeduplicateResults(parsedResults), nil
|
return core.DeduplicateResults(parsedResults), nil
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-3
@@ -97,11 +97,16 @@ func (bing *Bing) acceptCookies(ctx context.Context, page *rod.Page) error {
|
|||||||
// Search executes a Bing web search and returns normalized search results.
|
// Search executes a Bing web search and returns normalized search results.
|
||||||
// It may return core.ErrCaptcha or core.ErrSearchTimeout.
|
// It may return core.ErrCaptcha or core.ErrSearchTimeout.
|
||||||
func (bing *Bing) Search(ctx context.Context, query core.Query) (results []core.SearchResult, err error) {
|
func (bing *Bing) Search(ctx context.Context, query core.Query) (results []core.SearchResult, err error) {
|
||||||
ctx = core.EnsureContext(ctx)
|
ctx = core.WithEngine(core.EnsureContext(ctx), bing.Name())
|
||||||
|
ctx = core.WithQueryHash(ctx, core.QueryHashFromQuery(query))
|
||||||
|
scoped := *bing
|
||||||
|
scoped.logger = bing.logger.WithRequest(ctx)
|
||||||
|
bing = &scoped
|
||||||
|
|
||||||
bing.logger.Debug("Starting search, query: %+v", query)
|
bing.logger.Debug("Starting search, query: %+v", query)
|
||||||
defer func() {
|
defer func() {
|
||||||
if recovered := recover(); recovered != nil {
|
if recovered := recover(); recovered != nil {
|
||||||
err = core.RecoverEnginePanic(bing.Name(), recovered, bing.logger)
|
err = core.RecoverEnginePanicWithContext(ctx, bing.Name(), recovered, bing.logger)
|
||||||
results = nil
|
results = nil
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -268,7 +273,12 @@ type BingImageData struct {
|
|||||||
// SearchImage executes a Bing image search and returns normalized image
|
// SearchImage executes a Bing image search and returns normalized image
|
||||||
// results. It may return core.ErrCaptcha or core.ErrSearchTimeout.
|
// results. It may return core.ErrCaptcha or core.ErrSearchTimeout.
|
||||||
func (bing *Bing) SearchImage(ctx context.Context, query core.Query) ([]core.SearchResult, error) {
|
func (bing *Bing) SearchImage(ctx context.Context, query core.Query) ([]core.SearchResult, error) {
|
||||||
ctx = core.EnsureContext(ctx)
|
ctx = core.WithEngine(core.EnsureContext(ctx), bing.Name())
|
||||||
|
ctx = core.WithQueryHash(ctx, core.QueryHashFromQuery(query))
|
||||||
|
scoped := *bing
|
||||||
|
scoped.logger = bing.logger.WithRequest(ctx)
|
||||||
|
bing = &scoped
|
||||||
|
|
||||||
bing.logger.Debug("Starting image search, query: %+v", query)
|
bing.logger.Debug("Starting image search, query: %+v", query)
|
||||||
|
|
||||||
searchResults := []core.SearchResult{}
|
searchResults := []core.SearchResult{}
|
||||||
|
|||||||
+1
-1
@@ -33,7 +33,7 @@ func BuildURL(q core.Query) (string, error) {
|
|||||||
text += " filetype:" + q.Filetype
|
text += " filetype:" + q.Filetype
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Tracef("Query text: %s", text)
|
logrus.WithField("query_hash", core.QueryHash(text)).Trace(fmt.Sprintf("Query text: %s", text))
|
||||||
params.Add("q", text)
|
params.Add("q", text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-6
@@ -57,6 +57,7 @@ type AppConfig struct {
|
|||||||
IsLeaveHead bool `mapstructure:"leave_head"`
|
IsLeaveHead bool `mapstructure:"leave_head"`
|
||||||
IsLeakless bool `mapstructure:"leakless"`
|
IsLeakless bool `mapstructure:"leakless"`
|
||||||
IsStealth bool `mapstructure:"stealth"`
|
IsStealth bool `mapstructure:"stealth"`
|
||||||
|
LogFormat string `mapstructure:"log_format"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type EngineConfig struct {
|
type EngineConfig struct {
|
||||||
@@ -117,6 +118,7 @@ var flagToConfigKey = map[string]string{
|
|||||||
"cb_failures": "circuit_breaker.failures",
|
"cb_failures": "circuit_breaker.failures",
|
||||||
"cb_recovery": "circuit_breaker.recovery_seconds",
|
"cb_recovery": "circuit_breaker.recovery_seconds",
|
||||||
"cb_successes": "circuit_breaker.successes",
|
"cb_successes": "circuit_breaker.successes",
|
||||||
|
"log_format": "app.log_format",
|
||||||
}
|
}
|
||||||
|
|
||||||
var RootCmd = &cobra.Command{
|
var RootCmd = &cobra.Command{
|
||||||
@@ -131,8 +133,14 @@ var RootCmd = &cobra.Command{
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
core.InitLogger(config.Server.IsVerbose, config.Server.IsDebug)
|
logFormat, err := core.NormalizeLogFormat(config.App.LogFormat)
|
||||||
logrus.Debugf("Final config: %+v", config)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
config.App.LogFormat = logFormat
|
||||||
|
|
||||||
|
core.InitLogger(config.Server.IsVerbose, config.Server.IsDebug, config.App.LogFormat)
|
||||||
|
logrus.WithField("config", fmt.Sprintf("%+v", config)).Debug("Final config")
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -146,13 +154,13 @@ func bindFlags(cmd *cobra.Command, vpr *viper.Viper) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := vpr.BindPFlag(configName, flg); err != nil {
|
if err := vpr.BindPFlag(configName, flg); err != nil {
|
||||||
logrus.Errorf("Unable to bind flag %s: %v", flg.Name, err)
|
logrus.WithError(err).Error(fmt.Sprintf("Unable to bind flag %s: %v", flg.Name, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if flg.Changed {
|
if flg.Changed {
|
||||||
val, err := parseFlagValue(flg)
|
val, err := parseFlagValue(flg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf("Unable to parse flag %s: %v", flg.Name, err)
|
logrus.WithError(err).Error(fmt.Sprintf("Unable to parse flag %s: %v", flg.Name, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
vpr.Set(configName, val)
|
vpr.Set(configName, val)
|
||||||
@@ -206,7 +214,7 @@ func initializeConfig(cmd *cobra.Command) error {
|
|||||||
envKey := envPrefix + "_" + strings.ToUpper(strings.ReplaceAll(key, ".", "_"))
|
envKey := envPrefix + "_" + strings.ToUpper(strings.ReplaceAll(key, ".", "_"))
|
||||||
err := v.BindEnv(key, envKey)
|
err := v.BindEnv(key, envKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf("Unable to bind ENV valye: %v", err)
|
logrus.WithError(err).Error(fmt.Sprintf("Unable to bind ENV valye: %v", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,6 +310,7 @@ func setConfigDefaults(v *viper.Viper) {
|
|||||||
v.SetDefault("server.verbose", false)
|
v.SetDefault("server.verbose", false)
|
||||||
v.SetDefault("server.raw_requests", false)
|
v.SetDefault("server.raw_requests", false)
|
||||||
v.SetDefault("server.insecure", false)
|
v.SetDefault("server.insecure", false)
|
||||||
|
v.SetDefault("app.log_format", "")
|
||||||
|
|
||||||
v.SetDefault("app.timeout", 30)
|
v.SetDefault("app.timeout", 30)
|
||||||
v.SetDefault("app.browser_path", "")
|
v.SetDefault("app.browser_path", "")
|
||||||
@@ -325,7 +334,7 @@ func setConfigDefaults(v *viper.Viper) {
|
|||||||
v.SetDefault("cors.enabled", true)
|
v.SetDefault("cors.enabled", true)
|
||||||
v.SetDefault("cors.allow_origins", "*")
|
v.SetDefault("cors.allow_origins", "*")
|
||||||
v.SetDefault("cors.allow_methods", "GET, POST, OPTIONS")
|
v.SetDefault("cors.allow_methods", "GET, POST, OPTIONS")
|
||||||
v.SetDefault("cors.allow_headers", "Origin, Content-Type, Accept, Authorization, X-Use-Proxy")
|
v.SetDefault("cors.allow_headers", "Origin, Content-Type, Accept, Authorization, X-Use-Proxy, X-Request-ID, X-Tenant")
|
||||||
v.SetDefault("cors.max_age", 86400)
|
v.SetDefault("cors.max_age", 86400)
|
||||||
v.SetDefault("captcha.solver_enabled", false)
|
v.SetDefault("captcha.solver_enabled", false)
|
||||||
}
|
}
|
||||||
@@ -353,4 +362,5 @@ func init() {
|
|||||||
RootCmd.PersistentFlags().IntVar(&config.CircuitBreaker.Failures, "cb_failures", 5, "Consecutive failures before circuit breaker opens")
|
RootCmd.PersistentFlags().IntVar(&config.CircuitBreaker.Failures, "cb_failures", 5, "Consecutive failures before circuit breaker opens")
|
||||||
RootCmd.PersistentFlags().IntVar(&config.CircuitBreaker.RecoverySeconds, "cb_recovery", 60, "Seconds before retrying an engine with open circuit")
|
RootCmd.PersistentFlags().IntVar(&config.CircuitBreaker.RecoverySeconds, "cb_recovery", 60, "Seconds before retrying an engine with open circuit")
|
||||||
RootCmd.PersistentFlags().IntVar(&config.CircuitBreaker.Successes, "cb_successes", 2, "Consecutive successful half-open checks needed to close circuit")
|
RootCmd.PersistentFlags().IntVar(&config.CircuitBreaker.Successes, "cb_successes", 2, "Consecutive successful half-open checks needed to close circuit")
|
||||||
|
RootCmd.PersistentFlags().StringVar(&config.App.LogFormat, "log_format", "", "Log format: json or text (default: json in production, text in debug)")
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-8
@@ -37,7 +37,7 @@ func search(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
captchaSolverEnabled, captchaSolverAPIKey, err := resolveCaptchaSolverConfig()
|
captchaSolverEnabled, captchaSolverAPIKey, err := resolveCaptchaSolverConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf("Error validating captcha solver config: %v", err)
|
logrus.WithError(err).Error(fmt.Sprintf("Error validating captcha solver config: %v", err))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ func search(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
proxyCfg, err := buildNormalizedProxyConfig(proxyRuntime)
|
proxyCfg, err := buildNormalizedProxyConfig(proxyRuntime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf("Error validating proxy config: %v", err)
|
logrus.WithError(err).Error(fmt.Sprintf("Error validating proxy config: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ func search(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
selectedProxy, err := selectCLIProxy(proxyCfg, policy)
|
selectedProxy, err := selectCLIProxy(proxyCfg, policy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf("Error selecting proxy for %s: %v", engineType, err)
|
logrus.WithError(err).Error(fmt.Sprintf("Error selecting proxy for %s: %v", engineType, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,23 +64,29 @@ func search(cmd *cobra.Command, args []string) {
|
|||||||
query.ProxyURL = selectedProxy
|
query.ProxyURL = selectedProxy
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("Starting SERP search request using %s engine for query: %s", engineType, query.Text)
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"engine": engineType,
|
||||||
|
"query_hash": core.QueryHashFromQuery(query),
|
||||||
|
}).Info(fmt.Sprintf("Starting SERP search request using %s engine for query: %s", engineType, query.Text))
|
||||||
|
|
||||||
var results []core.SearchResult
|
var results []core.SearchResult
|
||||||
if config.Server.IsRawRequests {
|
if config.Server.IsRawRequests {
|
||||||
logrus.Infof("Using raw requests mode for %s search", engineType)
|
logrus.WithField("engine", engineType).Info(fmt.Sprintf("Using raw requests mode for %s search", engineType))
|
||||||
results, err = searchRaw(engineType, query)
|
results, err = searchRaw(engineType, query)
|
||||||
} else {
|
} else {
|
||||||
logrus.Infof("Using browser mode for %s search", engineType)
|
logrus.WithField("engine", engineType).Info(fmt.Sprintf("Using browser mode for %s search", engineType))
|
||||||
results, err = searchBrowser(engineType, query, selectedProxy, captchaSolverEnabled, captchaSolverAPIKey)
|
results, err = searchBrowser(engineType, query, selectedProxy, captchaSolverEnabled, captchaSolverAPIKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf("Error during %s search: %s", engineType, err)
|
logrus.WithError(err).WithField("engine", engineType).Error(fmt.Sprintf("Error during %s search: %s", engineType, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("Successfully completed SERP search using %s engine, returned %d results", engineType, len(results))
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"engine": engineType,
|
||||||
|
"results_count": len(results),
|
||||||
|
}).Info(fmt.Sprintf("Successfully completed SERP search using %s engine, returned %d results", engineType, len(results)))
|
||||||
|
|
||||||
b, err := json.MarshalIndent(results, "", " ")
|
b, err := json.MarshalIndent(results, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+1
-1
@@ -84,7 +84,7 @@ func serve(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
proxyCfg, err := buildNormalizedProxyConfig(proxyRuntime)
|
proxyCfg, err := buildNormalizedProxyConfig(proxyRuntime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf("invalid proxy configuration: %v", err)
|
logrus.WithError(err).Error(fmt.Sprintf("invalid proxy configuration: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ server:
|
|||||||
insecure: true # Allow insecure TLS connections
|
insecure: true # Allow insecure TLS connections
|
||||||
|
|
||||||
app:
|
app:
|
||||||
|
# json|text. Empty means auto: text in debug mode, json otherwise.
|
||||||
|
log_format: "text"
|
||||||
timeout: 15 # Browser/search timeout in seconds
|
timeout: 15 # Browser/search timeout in seconds
|
||||||
browser_path: "" # Custom browser binary path (chrome/chromium/edge..)
|
browser_path: "" # Custom browser binary path (chrome/chromium/edge..)
|
||||||
head: false # Show browser UI (headful mode)
|
head: false # Show browser UI (headful mode)
|
||||||
|
|||||||
+19
-10
@@ -71,7 +71,7 @@ type Browser struct {
|
|||||||
// Browser wrapper configured with proxy and captcha solver settings.
|
// Browser wrapper configured with proxy and captcha solver settings.
|
||||||
func NewBrowser(opts BrowserOpts) (*Browser, error) {
|
func NewBrowser(opts BrowserOpts) (*Browser, error) {
|
||||||
opts.Check()
|
opts.Check()
|
||||||
logrus.Debugf("Browser options: %+v", opts)
|
logrus.WithField("browser_options", fmt.Sprintf("%+v", opts)).Debug("Browser options")
|
||||||
|
|
||||||
path, err := resolveBrowserBinaryPath(opts.BrowserPath, launcher.LookPath)
|
path, err := resolveBrowserBinaryPath(opts.BrowserPath, launcher.LookPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -82,7 +82,7 @@ func NewBrowser(opts BrowserOpts) (*Browser, error) {
|
|||||||
l := launcher.New().Leakless(opts.IsLeakless).Headless(opts.IsHeadless).Set("disable-blink-features", "AutomationControlled").
|
l := launcher.New().Leakless(opts.IsLeakless).Headless(opts.IsHeadless).Set("disable-blink-features", "AutomationControlled").
|
||||||
Delete("enable-automation")
|
Delete("enable-automation")
|
||||||
if path != "" {
|
if path != "" {
|
||||||
logrus.Debugf("Using browser binary: %s", path)
|
logrus.WithField("browser_path", path).Debug("Using browser binary")
|
||||||
l = l.Bin(path)
|
l = l.Bin(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,13 +102,16 @@ func NewBrowser(opts BrowserOpts) (*Browser, error) {
|
|||||||
// Chrome's proxy-server flag must not contain credentials.
|
// Chrome's proxy-server flag must not contain credentials.
|
||||||
// Auth (if needed) is handled separately via DevTools auth callbacks.
|
// Auth (if needed) is handled separately via DevTools auth callbacks.
|
||||||
proxyStr := proxyURLForBrowserLaunch(proxyUrl)
|
proxyStr := proxyURLForBrowserLaunch(proxyUrl)
|
||||||
logrus.Debugf("Setting up proxy: %s", MaskProxyURL(proxyStr))
|
logrus.WithField("proxy", MaskProxyURL(proxyStr)).Debug("Setting up proxy")
|
||||||
l = l.Proxy(proxyStr)
|
l = l.Proxy(proxyStr)
|
||||||
|
|
||||||
// Check if proxy has auth credentials
|
// Check if proxy has auth credentials
|
||||||
if proxyUrl.User != nil {
|
if proxyUrl.User != nil {
|
||||||
username := proxyUrl.User.Username()
|
username := proxyUrl.User.Username()
|
||||||
logrus.Debugf("Proxy credentials configured for %s proxy: %s:****", proxyUrl.Scheme, username)
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"proxy_scheme": proxyUrl.Scheme,
|
||||||
|
"proxy_username": username,
|
||||||
|
}).Debugf("Proxy credentials configured for %s proxy: %s:****", proxyUrl.Scheme, username)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +190,7 @@ func (b *Browser) Navigate(ctx context.Context, URL string) (*rod.Page, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debug("Navigate to: ", URL)
|
WithRequest(ctx).WithField("url", URL).Debug("Navigate to")
|
||||||
|
|
||||||
browser := rod.New().ControlURL(b.browserAddr).Timeout(b.Timeout)
|
browser := rod.New().ControlURL(b.browserAddr).Timeout(b.Timeout)
|
||||||
if err := browser.Connect(); err != nil {
|
if err := browser.Connect(); err != nil {
|
||||||
@@ -214,7 +217,7 @@ func (b *Browser) Navigate(ctx context.Context, URL string) (*rod.Page, error) {
|
|||||||
// Launch auth handler before any navigation occurs
|
// Launch auth handler before any navigation occurs
|
||||||
go func() {
|
go func() {
|
||||||
if err := b.browser.HandleAuth(username, password)(); err != nil {
|
if err := b.browser.HandleAuth(username, password)(); err != nil {
|
||||||
logrus.Debugf("Proxy auth handler stopped: %v", err)
|
WithRequest(ctx).WithError(err).Debug("Proxy auth handler stopped")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
} else if proxyUrl.User != nil && (proxyUrl.Scheme == "socks5" || proxyUrl.Scheme == "socks5h") {
|
} else if proxyUrl.User != nil && (proxyUrl.Scheme == "socks5" || proxyUrl.Scheme == "socks5h") {
|
||||||
@@ -252,7 +255,7 @@ func (b *Browser) Navigate(ctx context.Context, URL string) (*rod.Page, error) {
|
|||||||
// when the caller context is canceled or navigation fails.
|
// when the caller context is canceled or navigation fails.
|
||||||
closeOnErr := func() {
|
closeOnErr := func() {
|
||||||
if cerr := page.Close(); cerr != nil {
|
if cerr := page.Close(); cerr != nil {
|
||||||
logrus.Debugf("Close page after navigate error failed: %v", cerr)
|
WithRequest(ctx).WithError(cerr).Debug("Close page after navigate error failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,9 +294,11 @@ func (b *Browser) Navigate(ctx context.Context, URL string) (*rod.Page, error) {
|
|||||||
if errors.Is(werr, context.DeadlineExceeded) {
|
if errors.Is(werr, context.DeadlineExceeded) {
|
||||||
// Some engines keep loading background resources while the DOM is already usable.
|
// Some engines keep loading background resources while the DOM is already usable.
|
||||||
// Treat load timeout as non-fatal and let engine-specific selector timeouts decide.
|
// Treat load timeout as non-fatal and let engine-specific selector timeouts decide.
|
||||||
logrus.Debugf("WaitLoad timed out after %s; continuing with partial page state", b.Timeout)
|
WithRequest(ctx).WithField("timeout", b.Timeout.String()).Debug(
|
||||||
|
fmt.Sprintf("WaitLoad timed out after %s; continuing with partial page state", b.Timeout),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
logrus.Debugf("WaitLoad returned early: %v", werr)
|
WithRequest(ctx).WithError(werr).Debug("WaitLoad returned early")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,11 +336,15 @@ func ClosePageWithTimeout(ctx context.Context, page *rod.Page, timeout time.Dura
|
|||||||
// RecoverEnginePanic converts recovered panics to a typed engine error and
|
// RecoverEnginePanic converts recovered panics to a typed engine error and
|
||||||
// logs stack trace with engine context.
|
// logs stack trace with engine context.
|
||||||
func RecoverEnginePanic(engine string, recovered interface{}, logger *EngineLogger) error {
|
func RecoverEnginePanic(engine string, recovered interface{}, logger *EngineLogger) error {
|
||||||
|
return RecoverEnginePanicWithContext(nil, engine, recovered, logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RecoverEnginePanicWithContext(ctx context.Context, engine string, recovered interface{}, logger *EngineLogger) error {
|
||||||
stack := debug.Stack()
|
stack := debug.Stack()
|
||||||
if logger != nil {
|
if logger != nil {
|
||||||
logger.Error("Recovered panic in %s Search: panic=%v\n%s", engine, recovered, string(stack))
|
logger.Error("Recovered panic in %s Search: panic=%v\n%s", engine, recovered, string(stack))
|
||||||
} else {
|
} else {
|
||||||
logrus.Errorf("Recovered panic in %s Search: panic=%v\n%s", engine, recovered, string(stack))
|
WithRequestEngine(ctx, engine).Errorf("Recovered panic in %s Search: panic=%v\n%s", engine, recovered, string(stack))
|
||||||
}
|
}
|
||||||
return fmt.Errorf("%w: %s", ErrEngineInternal, engine)
|
return fmt.Errorf("%w: %s", ErrEngineInternal, engine)
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-10
@@ -1,11 +1,10 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type CircuitState int
|
type CircuitState int
|
||||||
@@ -64,7 +63,7 @@ func NewCircuitBreaker(name string, cfg CircuitBreakerConfig) *CircuitBreaker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cb *CircuitBreaker) AllowRequest() bool {
|
func (cb *CircuitBreaker) AllowRequest(ctx context.Context) bool {
|
||||||
cb.mu.Lock()
|
cb.mu.Lock()
|
||||||
defer cb.mu.Unlock()
|
defer cb.mu.Unlock()
|
||||||
|
|
||||||
@@ -74,7 +73,7 @@ func (cb *CircuitBreaker) AllowRequest() bool {
|
|||||||
case CircuitOpen:
|
case CircuitOpen:
|
||||||
if time.Since(cb.lastFailureTime) >= cb.config.RecoveryTimeout {
|
if time.Since(cb.lastFailureTime) >= cb.config.RecoveryTimeout {
|
||||||
cb.setState(CircuitHalfOpen)
|
cb.setState(CircuitHalfOpen)
|
||||||
logrus.Infof("[CircuitBreaker][%s] Recovery timeout elapsed, moving to half-open", cb.name)
|
WithRequestEngine(ctx, cb.name).Info("Recovery timeout elapsed, moving to half-open")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -85,7 +84,7 @@ func (cb *CircuitBreaker) AllowRequest() bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cb *CircuitBreaker) RecordSuccess() {
|
func (cb *CircuitBreaker) RecordSuccess(ctx context.Context) {
|
||||||
cb.mu.Lock()
|
cb.mu.Lock()
|
||||||
defer cb.mu.Unlock()
|
defer cb.mu.Unlock()
|
||||||
|
|
||||||
@@ -96,14 +95,14 @@ func (cb *CircuitBreaker) RecordSuccess() {
|
|||||||
cb.setState(CircuitClosed)
|
cb.setState(CircuitClosed)
|
||||||
cb.failureCount = 0
|
cb.failureCount = 0
|
||||||
cb.successCount = 0
|
cb.successCount = 0
|
||||||
logrus.Infof("[CircuitBreaker][%s] Recovered, circuit closed", cb.name)
|
WithRequestEngine(ctx, cb.name).Info("Circuit recovered, closed")
|
||||||
}
|
}
|
||||||
case CircuitClosed:
|
case CircuitClosed:
|
||||||
cb.failureCount = 0
|
cb.failureCount = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cb *CircuitBreaker) RecordFailure() {
|
func (cb *CircuitBreaker) RecordFailure(ctx context.Context) {
|
||||||
cb.mu.Lock()
|
cb.mu.Lock()
|
||||||
defer cb.mu.Unlock()
|
defer cb.mu.Unlock()
|
||||||
|
|
||||||
@@ -114,13 +113,15 @@ func (cb *CircuitBreaker) RecordFailure() {
|
|||||||
cb.failureCount++
|
cb.failureCount++
|
||||||
if cb.failureCount >= cb.config.FailureThreshold {
|
if cb.failureCount >= cb.config.FailureThreshold {
|
||||||
cb.setState(CircuitOpen)
|
cb.setState(CircuitOpen)
|
||||||
logrus.Warnf("[CircuitBreaker][%s] Circuit OPENED after %d consecutive failures (will retry in %s)",
|
WithRequestEngine(ctx, cb.name).
|
||||||
cb.name, cb.failureCount, cb.config.RecoveryTimeout)
|
WithField("failure_count", cb.failureCount).
|
||||||
|
WithField("recovery_timeout", cb.config.RecoveryTimeout.String()).
|
||||||
|
Warn("Circuit opened after consecutive failures")
|
||||||
}
|
}
|
||||||
case CircuitHalfOpen:
|
case CircuitHalfOpen:
|
||||||
cb.setState(CircuitOpen)
|
cb.setState(CircuitOpen)
|
||||||
cb.successCount = 0
|
cb.successCount = 0
|
||||||
logrus.Warnf("[CircuitBreaker][%s] Failed during half-open, circuit re-opened", cb.name)
|
WithRequestEngine(ctx, cb.name).Warn("Failed during half-open, circuit re-opened")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -20,17 +21,17 @@ func TestCircuitBreaker_OpensAfterThreshold(t *testing.T) {
|
|||||||
}
|
}
|
||||||
cb := newTestCircuitBreaker(t, cfg)
|
cb := newTestCircuitBreaker(t, cfg)
|
||||||
|
|
||||||
cb.RecordFailure()
|
cb.RecordFailure(context.Background())
|
||||||
cb.RecordFailure()
|
cb.RecordFailure(context.Background())
|
||||||
if cb.State() != CircuitClosed {
|
if cb.State() != CircuitClosed {
|
||||||
t.Fatalf("expected closed after 2 failures, got: %s", cb.State())
|
t.Fatalf("expected closed after 2 failures, got: %s", cb.State())
|
||||||
}
|
}
|
||||||
|
|
||||||
cb.RecordFailure()
|
cb.RecordFailure(context.Background())
|
||||||
if cb.State() != CircuitOpen {
|
if cb.State() != CircuitOpen {
|
||||||
t.Fatalf("expected open after %d failures, got: %s", cfg.FailureThreshold, cb.State())
|
t.Fatalf("expected open after %d failures, got: %s", cfg.FailureThreshold, cb.State())
|
||||||
}
|
}
|
||||||
if cb.AllowRequest() {
|
if cb.AllowRequest(context.Background()) {
|
||||||
t.Error("expected request blocked in open state")
|
t.Error("expected request blocked in open state")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,14 +46,14 @@ func TestCircuitBreaker_RecoveryToHalfOpen(t *testing.T) {
|
|||||||
}
|
}
|
||||||
cb := newTestCircuitBreaker(t, cfg)
|
cb := newTestCircuitBreaker(t, cfg)
|
||||||
|
|
||||||
cb.RecordFailure()
|
cb.RecordFailure(context.Background())
|
||||||
cb.RecordFailure()
|
cb.RecordFailure(context.Background())
|
||||||
if cb.State() != CircuitOpen {
|
if cb.State() != CircuitOpen {
|
||||||
t.Fatal("expected open")
|
t.Fatal("expected open")
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(60 * time.Millisecond)
|
time.Sleep(60 * time.Millisecond)
|
||||||
if !cb.AllowRequest() {
|
if !cb.AllowRequest(context.Background()) {
|
||||||
t.Error("should allow request after recovery timeout")
|
t.Error("should allow request after recovery timeout")
|
||||||
}
|
}
|
||||||
if cb.State() != CircuitHalfOpen {
|
if cb.State() != CircuitHalfOpen {
|
||||||
@@ -70,25 +71,25 @@ func TestCircuitBreaker_HalfOpenSuccessClosesCircuit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
cb := newTestCircuitBreaker(t, cfg)
|
cb := newTestCircuitBreaker(t, cfg)
|
||||||
|
|
||||||
cb.RecordFailure()
|
cb.RecordFailure(context.Background())
|
||||||
if cb.State() != CircuitOpen {
|
if cb.State() != CircuitOpen {
|
||||||
t.Fatalf("expected open, got: %s", cb.State())
|
t.Fatalf("expected open, got: %s", cb.State())
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(30 * time.Millisecond)
|
time.Sleep(30 * time.Millisecond)
|
||||||
if !cb.AllowRequest() {
|
if !cb.AllowRequest(context.Background()) {
|
||||||
t.Fatal("expected request to pass in recovery window")
|
t.Fatal("expected request to pass in recovery window")
|
||||||
}
|
}
|
||||||
if cb.State() != CircuitHalfOpen {
|
if cb.State() != CircuitHalfOpen {
|
||||||
t.Fatalf("expected half-open after recovery timeout, got: %s", cb.State())
|
t.Fatalf("expected half-open after recovery timeout, got: %s", cb.State())
|
||||||
}
|
}
|
||||||
|
|
||||||
cb.RecordSuccess()
|
cb.RecordSuccess(context.Background())
|
||||||
if cb.State() != CircuitHalfOpen {
|
if cb.State() != CircuitHalfOpen {
|
||||||
t.Fatalf("expected to stay half-open until success threshold reached, got: %s", cb.State())
|
t.Fatalf("expected to stay half-open until success threshold reached, got: %s", cb.State())
|
||||||
}
|
}
|
||||||
|
|
||||||
cb.RecordSuccess()
|
cb.RecordSuccess(context.Background())
|
||||||
if cb.State() != CircuitClosed {
|
if cb.State() != CircuitClosed {
|
||||||
t.Fatalf("expected closed after success threshold reached, got: %s", cb.State())
|
t.Fatalf("expected closed after success threshold reached, got: %s", cb.State())
|
||||||
}
|
}
|
||||||
@@ -104,16 +105,16 @@ func TestCircuitBreaker_HalfOpenFailureReopens(t *testing.T) {
|
|||||||
}
|
}
|
||||||
cb := newTestCircuitBreaker(t, cfg)
|
cb := newTestCircuitBreaker(t, cfg)
|
||||||
|
|
||||||
cb.RecordFailure()
|
cb.RecordFailure(context.Background())
|
||||||
time.Sleep(30 * time.Millisecond)
|
time.Sleep(30 * time.Millisecond)
|
||||||
if !cb.AllowRequest() {
|
if !cb.AllowRequest(context.Background()) {
|
||||||
t.Fatal("expected probe request in half-open")
|
t.Fatal("expected probe request in half-open")
|
||||||
}
|
}
|
||||||
if cb.State() != CircuitHalfOpen {
|
if cb.State() != CircuitHalfOpen {
|
||||||
t.Fatalf("expected half-open, got: %s", cb.State())
|
t.Fatalf("expected half-open, got: %s", cb.State())
|
||||||
}
|
}
|
||||||
|
|
||||||
cb.RecordFailure()
|
cb.RecordFailure(context.Background())
|
||||||
if cb.State() != CircuitOpen {
|
if cb.State() != CircuitOpen {
|
||||||
t.Fatalf("expected open after failed half-open probe, got: %s", cb.State())
|
t.Fatalf("expected open after failed half-open probe, got: %s", cb.State())
|
||||||
}
|
}
|
||||||
@@ -123,7 +124,7 @@ func TestCircuitBreaker_HalfOpenFailureReopens(t *testing.T) {
|
|||||||
// only when breaker is open.
|
// only when breaker is open.
|
||||||
func TestCircuitBreaker_Stats(t *testing.T) {
|
func TestCircuitBreaker_Stats(t *testing.T) {
|
||||||
cb := NewCircuitBreaker("test-engine", DefaultCircuitBreakerConfig())
|
cb := NewCircuitBreaker("test-engine", DefaultCircuitBreakerConfig())
|
||||||
cb.RecordFailure()
|
cb.RecordFailure(context.Background())
|
||||||
|
|
||||||
stats := cb.Stats()
|
stats := cb.Stats()
|
||||||
if stats["engine"] != "test-engine" {
|
if stats["engine"] != "test-engine" {
|
||||||
@@ -141,7 +142,7 @@ func TestCircuitBreaker_Stats(t *testing.T) {
|
|||||||
|
|
||||||
openCfg := CircuitBreakerConfig{FailureThreshold: 1, RecoveryTimeout: time.Second, SuccessThreshold: 1}
|
openCfg := CircuitBreakerConfig{FailureThreshold: 1, RecoveryTimeout: time.Second, SuccessThreshold: 1}
|
||||||
openCB := NewCircuitBreaker("open-engine", openCfg)
|
openCB := NewCircuitBreaker("open-engine", openCfg)
|
||||||
openCB.RecordFailure()
|
openCB.RecordFailure(context.Background())
|
||||||
openStats := openCB.Stats()
|
openStats := openCB.Stats()
|
||||||
retryIn, ok := openStats["retry_in"].(int64)
|
retryIn, ok := openStats["retry_in"].(int64)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
+236
-94
@@ -1,132 +1,274 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type customFormatter struct {
|
type loggerContextKey string
|
||||||
logrus.TextFormatter
|
|
||||||
|
const (
|
||||||
|
requestIDContextKey loggerContextKey = "request_id"
|
||||||
|
tenantContextKey loggerContextKey = "tenant"
|
||||||
|
engineContextKey loggerContextKey = "engine"
|
||||||
|
queryHashContextKey loggerContextKey = "query_hash"
|
||||||
|
|
||||||
|
LogFormatJSON = "json"
|
||||||
|
LogFormatText = "text"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NormalizeLogFormat(raw string) (string, error) {
|
||||||
|
format := strings.ToLower(strings.TrimSpace(raw))
|
||||||
|
if format == "" {
|
||||||
|
return LogFormatText, nil
|
||||||
|
}
|
||||||
|
switch format {
|
||||||
|
case LogFormatJSON, LogFormatText:
|
||||||
|
return format, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("invalid logging.format %q: expected json or text", raw)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *customFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
func WithRequestID(ctx context.Context, requestID string) context.Context {
|
||||||
message := entry.Message
|
requestID = strings.TrimSpace(requestID)
|
||||||
|
if requestID == "" {
|
||||||
// Check if engine name is provided as a field
|
return EnsureContext(ctx)
|
||||||
engineName := ""
|
|
||||||
if engine, exists := entry.Data["engine"]; exists {
|
|
||||||
if engineStr, ok := engine.(string); ok {
|
|
||||||
engineName = engineStr
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return context.WithValue(EnsureContext(ctx), requestIDContextKey, requestID)
|
||||||
// Format: [timestamp][level][engine] message
|
|
||||||
if engineName != "" {
|
|
||||||
return []byte(fmt.Sprintf("[%s][%s][%s] %s\n",
|
|
||||||
entry.Time.Format(f.TimestampFormat),
|
|
||||||
strings.ToUpper(entry.Level.String()),
|
|
||||||
engineName,
|
|
||||||
message)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format: [timestamp][level] message (no engine)
|
|
||||||
return []byte(fmt.Sprintf("[%s][%s] %s\n",
|
|
||||||
entry.Time.Format(f.TimestampFormat),
|
|
||||||
strings.ToUpper(entry.Level.String()),
|
|
||||||
message)), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EngineLogger provides simplified logging for search engines
|
func WithTenant(ctx context.Context, tenant string) context.Context {
|
||||||
|
tenant = strings.TrimSpace(tenant)
|
||||||
|
if tenant == "" {
|
||||||
|
return EnsureContext(ctx)
|
||||||
|
}
|
||||||
|
return context.WithValue(EnsureContext(ctx), tenantContextKey, tenant)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithEngine(ctx context.Context, engine string) context.Context {
|
||||||
|
engine = strings.TrimSpace(engine)
|
||||||
|
if engine == "" {
|
||||||
|
return EnsureContext(ctx)
|
||||||
|
}
|
||||||
|
return context.WithValue(EnsureContext(ctx), engineContextKey, engine)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithQueryHash(ctx context.Context, queryHash string) context.Context {
|
||||||
|
queryHash = strings.TrimSpace(queryHash)
|
||||||
|
if queryHash == "" {
|
||||||
|
return EnsureContext(ctx)
|
||||||
|
}
|
||||||
|
return context.WithValue(EnsureContext(ctx), queryHashContextKey, queryHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RequestIDFromContext(ctx context.Context) string {
|
||||||
|
value, _ := EnsureContext(ctx).Value(requestIDContextKey).(string)
|
||||||
|
return strings.TrimSpace(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithRequest(ctx context.Context) *logrus.Entry {
|
||||||
|
ctx = EnsureContext(ctx)
|
||||||
|
fields := logrus.Fields{}
|
||||||
|
|
||||||
|
if requestID, ok := ctx.Value(requestIDContextKey).(string); ok && strings.TrimSpace(requestID) != "" {
|
||||||
|
fields["request_id"] = strings.TrimSpace(requestID)
|
||||||
|
}
|
||||||
|
if tenant, ok := ctx.Value(tenantContextKey).(string); ok && strings.TrimSpace(tenant) != "" {
|
||||||
|
fields["tenant"] = strings.TrimSpace(tenant)
|
||||||
|
}
|
||||||
|
if engine, ok := ctx.Value(engineContextKey).(string); ok && strings.TrimSpace(engine) != "" {
|
||||||
|
fields["engine"] = strings.TrimSpace(engine)
|
||||||
|
}
|
||||||
|
if queryHash, ok := ctx.Value(queryHashContextKey).(string); ok && strings.TrimSpace(queryHash) != "" {
|
||||||
|
fields["query_hash"] = strings.TrimSpace(queryHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
return logrus.WithFields(fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithRequestEngine(ctx context.Context, engine string) *logrus.Entry {
|
||||||
|
return WithRequest(WithEngine(ctx, engine))
|
||||||
|
}
|
||||||
|
|
||||||
|
func QueryHash(raw string) string {
|
||||||
|
normalized := strings.TrimSpace(strings.ToLower(raw))
|
||||||
|
if normalized == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
hash := md5.Sum([]byte(normalized))
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func QueryHashFromQuery(q Query) string {
|
||||||
|
raw := strings.Join([]string{
|
||||||
|
q.Text,
|
||||||
|
q.Site,
|
||||||
|
q.Filetype,
|
||||||
|
q.LangCode,
|
||||||
|
q.DateInterval,
|
||||||
|
}, "|")
|
||||||
|
return QueryHash(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMessage(message string, args ...any) string {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(message, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EngineLogger provides structured logging for search engines with a fixed engine field.
|
||||||
type EngineLogger struct {
|
type EngineLogger struct {
|
||||||
engine string
|
engine string
|
||||||
logger *logrus.Entry
|
entry *logrus.Entry
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEngineLogger creates a new logger for a specific search engine
|
|
||||||
func NewEngineLogger(engine string) *EngineLogger {
|
func NewEngineLogger(engine string) *EngineLogger {
|
||||||
return &EngineLogger{
|
engine = strings.ToLower(strings.TrimSpace(engine))
|
||||||
engine: engine,
|
return &EngineLogger{engine: engine, entry: logrus.WithField("engine", engine)}
|
||||||
logger: logrus.WithField("engine", engine),
|
}
|
||||||
|
|
||||||
|
func (el *EngineLogger) WithRequest(ctx context.Context) *EngineLogger {
|
||||||
|
return &EngineLogger{engine: el.engine, entry: WithRequestEngine(ctx, el.engine)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields returns a new EngineLogger with additional structured fields merged in.
|
||||||
|
func (el *EngineLogger) Fields(fields logrus.Fields) *EngineLogger {
|
||||||
|
return &EngineLogger{engine: el.engine, entry: el.entry.WithFields(fields)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (el *EngineLogger) Debug(message string, args ...any) {
|
||||||
|
el.entry.Debug(formatMessage(message, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (el *EngineLogger) Info(message string, args ...any) {
|
||||||
|
el.entry.Info(formatMessage(message, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (el *EngineLogger) Warn(message string, args ...any) {
|
||||||
|
el.entry.Warn(formatMessage(message, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (el *EngineLogger) Error(message string, args ...any) {
|
||||||
|
el.entry.Error(formatMessage(message, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (el *EngineLogger) Fatal(message string, args ...any) {
|
||||||
|
el.entry.Fatal(formatMessage(message, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (el *EngineLogger) Panic(message string, args ...any) {
|
||||||
|
el.entry.Panic(formatMessage(message, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// bracketFormatter emits bracket-delimited fields:
|
||||||
|
//
|
||||||
|
// [time][level][engine=..][request_id=..][query_hash=..][extra fields sorted][msg]
|
||||||
|
//
|
||||||
|
// request_id is truncated to last 8 chars; query_hash to first 12.
|
||||||
|
type bracketFormatter struct {
|
||||||
|
TimestampFormat string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *bracketFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||||
|
ts := entry.Time.Format(f.TimestampFormat)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fmt.Fprintf(&buf, "[%s][%s]", ts, entry.Level.String())
|
||||||
|
|
||||||
|
// Context identity fields in fixed order, then remaining fields sorted, then msg last.
|
||||||
|
priority := []string{"engine", "tenant", "request_id", "query_hash"}
|
||||||
|
written := make(map[string]bool, len(entry.Data))
|
||||||
|
|
||||||
|
for _, key := range priority {
|
||||||
|
val, ok := entry.Data[key]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s := fmt.Sprintf("%v", val)
|
||||||
|
switch key {
|
||||||
|
case "request_id":
|
||||||
|
if len(s) > 8 {
|
||||||
|
s = s[len(s)-8:]
|
||||||
|
}
|
||||||
|
case "query_hash":
|
||||||
|
if len(s) > 12 {
|
||||||
|
s = s[:12]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&buf, "[%s=%s]", key, quoteIfNeeded(s))
|
||||||
|
written[key] = true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Debug logs a debug message
|
rest := make([]string, 0, len(entry.Data))
|
||||||
func (el *EngineLogger) Debug(message string, args ...interface{}) {
|
for k := range entry.Data {
|
||||||
el.logger.Debugf(message, args...)
|
if !written[k] {
|
||||||
}
|
rest = append(rest, k)
|
||||||
|
}
|
||||||
// Info logs an info message
|
|
||||||
func (el *EngineLogger) Info(message string, args ...interface{}) {
|
|
||||||
el.logger.Infof(message, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warn logs a warning message
|
|
||||||
func (el *EngineLogger) Warn(message string, args ...interface{}) {
|
|
||||||
el.logger.Warnf(message, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error logs an error message
|
|
||||||
func (el *EngineLogger) Error(message string, args ...interface{}) {
|
|
||||||
el.logger.Errorf(message, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fatal logs a fatal message
|
|
||||||
func (el *EngineLogger) Fatal(message string, args ...interface{}) {
|
|
||||||
el.logger.Fatalf(message, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Panic logs a panic message
|
|
||||||
func (el *EngineLogger) Panic(message string, args ...interface{}) {
|
|
||||||
el.logger.Panicf(message, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogWithEngine logs a message with engine information (deprecated - use EngineLogger instead)
|
|
||||||
func LogWithEngine(level logrus.Level, engine, message string, args ...interface{}) {
|
|
||||||
entry := logrus.WithField("engine", engine)
|
|
||||||
switch level {
|
|
||||||
case logrus.DebugLevel:
|
|
||||||
entry.Debugf(message, args...)
|
|
||||||
case logrus.InfoLevel:
|
|
||||||
entry.Infof(message, args...)
|
|
||||||
case logrus.WarnLevel:
|
|
||||||
entry.Warnf(message, args...)
|
|
||||||
case logrus.ErrorLevel:
|
|
||||||
entry.Errorf(message, args...)
|
|
||||||
case logrus.FatalLevel:
|
|
||||||
entry.Fatalf(message, args...)
|
|
||||||
case logrus.PanicLevel:
|
|
||||||
entry.Panicf(message, args...)
|
|
||||||
}
|
}
|
||||||
|
sort.Strings(rest)
|
||||||
|
for _, k := range rest {
|
||||||
|
fmt.Fprintf(&buf, "[%s=%s]", k, quoteIfNeeded(fmt.Sprintf("%v", entry.Data[k])))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message last so context fields are scannable without scrolling past a long msg.
|
||||||
|
if entry.Message != "" {
|
||||||
|
fmt.Fprintf(&buf, "[%s]", quoteIfNeeded(entry.Message))
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteByte('\n')
|
||||||
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitLogger(isVerbose, isDebug bool) {
|
// quoteIfNeeded wraps s in double-quotes if it contains spaces.
|
||||||
logrus.SetFormatter(&customFormatter{logrus.TextFormatter{
|
func quoteIfNeeded(s string) string {
|
||||||
FullTimestamp: true,
|
if strings.ContainsAny(s, " \t") {
|
||||||
TimestampFormat: "2006-01-02 15:04:05",
|
return `"` + strings.ReplaceAll(s, `"`, `\"`) + `"`
|
||||||
ForceColors: true,
|
}
|
||||||
DisableLevelTruncation: true,
|
return s
|
||||||
}})
|
}
|
||||||
|
|
||||||
if isVerbose {
|
func InitLogger(isVerbose, isDebug bool, format string) {
|
||||||
logrus.SetLevel(logrus.DebugLevel)
|
switch format {
|
||||||
|
case LogFormatText:
|
||||||
|
logrus.SetFormatter(&bracketFormatter{TimestampFormat: "2006-01-02 15:04:05"})
|
||||||
|
case LogFormatJSON:
|
||||||
|
logrus.SetFormatter(&logrus.JSONFormatter{
|
||||||
|
TimestampFormat: time.RFC3339Nano,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if isDebug {
|
if isDebug {
|
||||||
logrus.SetOutput(io.MultiWriter(os.Stdout))
|
logrus.SetOutput(io.MultiWriter(os.Stdout))
|
||||||
logrus.SetLevel(logrus.TraceLevel)
|
|
||||||
logrus.SetReportCaller(true)
|
logrus.SetReportCaller(true)
|
||||||
} else {
|
} else {
|
||||||
f, err := os.OpenFile("./logs.txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
|
f, err := os.OpenFile("./logs.txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Failed to create logsfile: ./logs.txt")
|
fmt.Fprintf(os.Stderr, "Failed to open logs file ./logs.txt: %v\n", err)
|
||||||
panic(err)
|
logrus.SetOutput(io.MultiWriter(os.Stdout))
|
||||||
|
} else {
|
||||||
|
logrus.SetOutput(io.MultiWriter(f, os.Stdout))
|
||||||
}
|
}
|
||||||
|
logrus.SetReportCaller(false)
|
||||||
logrus.SetOutput(io.MultiWriter(f, os.Stdout))
|
|
||||||
logrus.SetLevel(logrus.DebugLevel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
level := logrus.InfoLevel
|
||||||
|
if isVerbose {
|
||||||
|
level = logrus.DebugLevel
|
||||||
|
}
|
||||||
|
if isDebug {
|
||||||
|
level = logrus.TraceLevel
|
||||||
|
}
|
||||||
|
logrus.SetLevel(level)
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-11
@@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,11 +27,33 @@ func DefaultCORSConfig() CORSConfig {
|
|||||||
return CORSConfig{
|
return CORSConfig{
|
||||||
AllowOrigins: "*",
|
AllowOrigins: "*",
|
||||||
AllowMethods: "GET, POST, OPTIONS",
|
AllowMethods: "GET, POST, OPTIONS",
|
||||||
AllowHeaders: "Origin, Content-Type, Accept, Authorization, X-Use-Proxy",
|
AllowHeaders: "Origin, Content-Type, Accept, Authorization, X-Use-Proxy, X-Request-ID, X-Tenant",
|
||||||
MaxAge: 86400,
|
MaxAge: 86400,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RequestContextMiddleware() fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
requestID := strings.TrimSpace(c.Get("X-Request-ID"))
|
||||||
|
if requestID == "" {
|
||||||
|
id, err := uuid.NewV7()
|
||||||
|
if err != nil {
|
||||||
|
requestID = uuid.NewString()
|
||||||
|
} else {
|
||||||
|
requestID = id.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestCtx := WithRequestID(c.UserContext(), requestID)
|
||||||
|
requestCtx = WithTenant(requestCtx, strings.TrimSpace(c.Get("X-Tenant")))
|
||||||
|
requestCtx = WithQueryHash(requestCtx, QueryHash(c.Query("text")))
|
||||||
|
c.SetUserContext(requestCtx)
|
||||||
|
|
||||||
|
c.Set("X-Request-ID", requestID)
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func CORSMiddleware(cfg CORSConfig) fiber.Handler {
|
func CORSMiddleware(cfg CORSConfig) fiber.Handler {
|
||||||
cfg = normalizeCORSConfig(cfg)
|
cfg = normalizeCORSConfig(cfg)
|
||||||
|
|
||||||
@@ -83,23 +106,23 @@ func RequestLoggerMiddleware() fiber.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logFields := logrus.Fields{
|
logFields := logrus.Fields{
|
||||||
"method": c.Method(),
|
"method": c.Method(),
|
||||||
"path": c.Path(),
|
"path": c.Path(),
|
||||||
"status": status,
|
"status": status,
|
||||||
"latency": latency.String(),
|
"ip": c.IP(),
|
||||||
"ip": c.IP(),
|
|
||||||
}
|
}
|
||||||
|
logFields["latency_ms"] = latency.Milliseconds()
|
||||||
if query := c.Query("text"); query != "" {
|
if query := c.Query("text"); query != "" {
|
||||||
logFields["query"] = query
|
logFields["query_hash"] = QueryHash(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
entry := logrus.WithFields(logFields)
|
entry := WithRequest(c.UserContext()).WithFields(logFields)
|
||||||
if status >= 500 {
|
if status >= 500 {
|
||||||
entry.Errorf("%s - request failed", c.Path())
|
entry.Error("request failed")
|
||||||
} else if status >= 400 {
|
} else if status >= 400 {
|
||||||
entry.Warnf("%s - request error", c.Path())
|
entry.Warn("request error")
|
||||||
} else {
|
} else {
|
||||||
entry.Infof("%s - request completed", c.Path())
|
entry.Info("request completed")
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -107,3 +108,57 @@ func TestDefaultCORSConfig_IncludesProxyOverrideHeader(t *testing.T) {
|
|||||||
t.Fatalf("expected allow_headers to include X-Use-Proxy, got %q", got)
|
t.Fatalf("expected allow_headers to include X-Use-Proxy, got %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRequestContextMiddleware_EchoesProvidedRequestID(t *testing.T) {
|
||||||
|
app := fiber.New()
|
||||||
|
app.Use(RequestContextMiddleware())
|
||||||
|
app.Get("/id", func(c *fiber.Ctx) error {
|
||||||
|
return c.SendString(RequestIDFromContext(c.UserContext()))
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/id", nil)
|
||||||
|
req.Header.Set("X-Request-ID", "foo")
|
||||||
|
resp, err := app.Test(req, -1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := resp.Header.Get("X-Request-ID"); got != "foo" {
|
||||||
|
t.Fatalf("expected response X-Request-ID=foo, got %q", got)
|
||||||
|
}
|
||||||
|
if got := readBody(t, resp); got != "foo" {
|
||||||
|
t.Fatalf("expected context request id to be echoed, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestContextMiddleware_GeneratesRequestID(t *testing.T) {
|
||||||
|
app := fiber.New()
|
||||||
|
app.Use(RequestContextMiddleware())
|
||||||
|
app.Get("/id", func(c *fiber.Ctx) error {
|
||||||
|
return c.SendString(RequestIDFromContext(c.UserContext()))
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/id", nil)
|
||||||
|
resp, err := app.Test(req, -1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
requestID := resp.Header.Get("X-Request-ID")
|
||||||
|
if requestID == "" {
|
||||||
|
t.Fatal("expected generated X-Request-ID")
|
||||||
|
}
|
||||||
|
if got := readBody(t, resp); got != requestID {
|
||||||
|
t.Fatalf("expected request id in context to match header: body=%q header=%q", got, requestID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBody(t *testing.T, resp *http.Response) string {
|
||||||
|
t.Helper()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read body failed: %v", err)
|
||||||
|
}
|
||||||
|
return string(body)
|
||||||
|
}
|
||||||
|
|||||||
+18
-5
@@ -1,6 +1,7 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -351,6 +352,10 @@ func NewProxyRegistry(entries []ProxyEntryConfig, failureThreshold int) (*ProxyR
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *ProxyRegistry) NextByTag(tag string) string {
|
func (r *ProxyRegistry) NextByTag(tag string) string {
|
||||||
|
return r.NextByTagWithContext(nil, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProxyRegistry) NextByTagWithContext(ctx context.Context, tag string) string {
|
||||||
tag = normalizeTag(tag)
|
tag = normalizeTag(tag)
|
||||||
if tag == "" {
|
if tag == "" {
|
||||||
return ""
|
return ""
|
||||||
@@ -365,7 +370,9 @@ func (r *ProxyRegistry) NextByTag(tag string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if r.allDisabledLocked(urls) {
|
if r.allDisabledLocked(urls) {
|
||||||
logrus.Warnf("Proxy tag pool exhausted for %q, re-enabling tagged proxies", tag)
|
WithRequest(ctx).WithField("proxy_tag", tag).Warn(
|
||||||
|
fmt.Sprintf("Proxy tag pool exhausted for %q, re-enabling tagged proxies", tag),
|
||||||
|
)
|
||||||
for _, proxyURL := range urls {
|
for _, proxyURL := range urls {
|
||||||
state := r.states[proxyURL]
|
state := r.states[proxyURL]
|
||||||
state.disabled = false
|
state.disabled = false
|
||||||
@@ -383,14 +390,17 @@ func (r *ProxyRegistry) NextByTag(tag string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
r.nextByTag[tag] = (idx + 1) % len(urls)
|
r.nextByTag[tag] = (idx + 1) % len(urls)
|
||||||
logrus.Debugf("Selected proxy for tag=%s: %s", tag, MaskProxyURL(proxyURL))
|
WithRequest(ctx).WithFields(logrus.Fields{
|
||||||
|
"proxy_tag": tag,
|
||||||
|
"proxy": MaskProxyURL(proxyURL),
|
||||||
|
}).Debugf("Selected proxy for tag=%s: %s", tag, MaskProxyURL(proxyURL))
|
||||||
return proxyURL
|
return proxyURL
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ProxyRegistry) ReportFailure(proxyURL string) {
|
func (r *ProxyRegistry) ReportFailure(ctx context.Context, proxyURL string) {
|
||||||
proxyURL, err := NormalizeProxyURL(proxyURL)
|
proxyURL, err := NormalizeProxyURL(proxyURL)
|
||||||
if err != nil || proxyURL == "" {
|
if err != nil || proxyURL == "" {
|
||||||
return
|
return
|
||||||
@@ -407,11 +417,14 @@ func (r *ProxyRegistry) ReportFailure(proxyURL string) {
|
|||||||
state.failures++
|
state.failures++
|
||||||
if state.failures >= r.failureThreshold {
|
if state.failures >= r.failureThreshold {
|
||||||
state.disabled = true
|
state.disabled = true
|
||||||
logrus.Warnf("Disabled proxy after %d failures: %s", state.failures, MaskProxyURL(proxyURL))
|
WithRequest(ctx).WithFields(logrus.Fields{
|
||||||
|
"failure_count": state.failures,
|
||||||
|
"proxy": MaskProxyURL(proxyURL),
|
||||||
|
}).Warnf("Disabled proxy after %d failures: %s", state.failures, MaskProxyURL(proxyURL))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ProxyRegistry) ReportSuccess(proxyURL string) {
|
func (r *ProxyRegistry) ReportSuccess(_ context.Context, proxyURL string) {
|
||||||
proxyURL, err := NormalizeProxyURL(proxyURL)
|
proxyURL, err := NormalizeProxyURL(proxyURL)
|
||||||
if err != nil || proxyURL == "" {
|
if err != nil || proxyURL == "" {
|
||||||
return
|
return
|
||||||
|
|||||||
+7
-6
@@ -1,6 +1,7 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
@@ -141,20 +142,20 @@ func TestProxyRegistryRoundRobinAndFailureRecovery(t *testing.T) {
|
|||||||
t.Fatalf("expected second proxy2, got %s", got)
|
t.Fatalf("expected second proxy2, got %s", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
registry.ReportFailure("http://proxy1:8080")
|
registry.ReportFailure(context.Background(), "http://proxy1:8080")
|
||||||
registry.ReportFailure("http://proxy1:8080")
|
registry.ReportFailure(context.Background(), "http://proxy1:8080")
|
||||||
if got := registry.NextByTag("default"); got != "http://proxy2:8080" {
|
if got := registry.NextByTag("default"); got != "http://proxy2:8080" {
|
||||||
t.Fatalf("expected proxy2 while proxy1 disabled, got %s", got)
|
t.Fatalf("expected proxy2 while proxy1 disabled, got %s", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
registry.ReportFailure("http://proxy2:8080")
|
registry.ReportFailure(context.Background(), "http://proxy2:8080")
|
||||||
registry.ReportFailure("http://proxy2:8080")
|
registry.ReportFailure(context.Background(), "http://proxy2:8080")
|
||||||
if got := registry.NextByTag("default"); got != "http://proxy1:8080" {
|
if got := registry.NextByTag("default"); got != "http://proxy1:8080" {
|
||||||
t.Fatalf("expected tag pool reset to proxy1 after exhaustion, got %s", got)
|
t.Fatalf("expected tag pool reset to proxy1 after exhaustion, got %s", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
registry.ReportFailure("http://proxy1:8080")
|
registry.ReportFailure(context.Background(), "http://proxy1:8080")
|
||||||
registry.ReportSuccess("http://proxy1:8080")
|
registry.ReportSuccess(context.Background(), "http://proxy1:8080")
|
||||||
stats := registry.BuildStats()
|
stats := registry.BuildStats()
|
||||||
if stats.UnhealthyCount != 0 {
|
if stats.UnhealthyCount != 0 {
|
||||||
t.Fatalf("expected no unhealthy proxies after success recovery, got %d", stats.UnhealthyCount)
|
t.Fatalf("expected no unhealthy proxies after success recovery, got %d", stats.UnhealthyCount)
|
||||||
|
|||||||
+28
-23
@@ -47,7 +47,7 @@ func DefaultResilientConfig() ResilientConfig {
|
|||||||
func NewResilientSearcher(engines []SearchEngine, cfg ResilientConfig) *ResilientSearcher {
|
func NewResilientSearcher(engines []SearchEngine, cfg ResilientConfig) *ResilientSearcher {
|
||||||
proxyCfg, err := NormalizeProxyConfig(cfg.Proxy)
|
proxyCfg, err := NormalizeProxyConfig(cfg.Proxy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf("Invalid proxy config, using defaults: %v", err)
|
logrus.WithError(err).Error("Invalid proxy config, using defaults")
|
||||||
proxyCfg = DefaultProxyConfig()
|
proxyCfg = DefaultProxyConfig()
|
||||||
proxyCfg, _ = NormalizeProxyConfig(proxyCfg)
|
proxyCfg, _ = NormalizeProxyConfig(proxyCfg)
|
||||||
}
|
}
|
||||||
@@ -110,18 +110,18 @@ func (rs *ResilientSearcher) searchWithFallback(ctx context.Context, primaryEngi
|
|||||||
return nil, primaryEngine.Name(), proxyMeta, ctx.Err()
|
return nil, primaryEngine.Name(), proxyMeta, ctx.Err()
|
||||||
}
|
}
|
||||||
if errors.Is(err, ErrProxyUnavailable) {
|
if errors.Is(err, ErrProxyUnavailable) {
|
||||||
logrus.Warnf("[Resilient] Primary engine %s proxy policy failed closed: %s", primaryEngine.Name(), err)
|
WithRequestEngine(ctx, primaryEngine.Name()).WithError(err).Warn("Proxy policy failed closed")
|
||||||
return nil, primaryEngine.Name(), proxyMeta, err
|
return nil, primaryEngine.Name(), proxyMeta, err
|
||||||
}
|
}
|
||||||
|
|
||||||
action := "failed"
|
|
||||||
successMessage := "Fallback to %s succeeded with %d results"
|
successMessage := "Fallback to %s succeeded with %d results"
|
||||||
if isImage {
|
if isImage {
|
||||||
action = "image search failed"
|
|
||||||
successMessage = "Image fallback to %s succeeded with %d results"
|
successMessage = "Image fallback to %s succeeded with %d results"
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Warnf("[Resilient] Primary engine %s %s: %s. Trying fallback engines...", primaryEngine.Name(), action, err)
|
WithRequestEngine(ctx, primaryEngine.Name()).
|
||||||
|
WithError(err).
|
||||||
|
Warn("Primary engine failed, trying fallbacks")
|
||||||
for _, fallbackEngine := range rs.engines {
|
for _, fallbackEngine := range rs.engines {
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return nil, primaryEngine.Name(), proxyMeta, ctx.Err()
|
return nil, primaryEngine.Name(), proxyMeta, ctx.Err()
|
||||||
@@ -132,10 +132,12 @@ func (rs *ResilientSearcher) searchWithFallback(ctx context.Context, primaryEngi
|
|||||||
|
|
||||||
results, fallbackMeta, fallbackErr := rs.searchWithProtection(ctx, fallbackEngine, q, isImage)
|
results, fallbackMeta, fallbackErr := rs.searchWithProtection(ctx, fallbackEngine, q, isImage)
|
||||||
if fallbackErr == nil {
|
if fallbackErr == nil {
|
||||||
logrus.Infof("[Resilient] "+successMessage, fallbackEngine.Name(), len(results))
|
WithRequestEngine(ctx, fallbackEngine.Name()).
|
||||||
|
WithField("results_count", len(results)).
|
||||||
|
Infof(successMessage, fallbackEngine.Name(), len(results))
|
||||||
return results, fallbackEngine.Name(), fallbackMeta, nil
|
return results, fallbackEngine.Name(), fallbackMeta, nil
|
||||||
}
|
}
|
||||||
logrus.Warnf("[Resilient] Fallback engine %s also failed: %s", fallbackEngine.Name(), fallbackErr)
|
WithRequestEngine(ctx, fallbackEngine.Name()).WithError(fallbackErr).Debug("Fallback engine also failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, primaryEngine.Name(), proxyMeta, ErrAllEnginesFailed
|
return nil, primaryEngine.Name(), proxyMeta, ErrAllEnginesFailed
|
||||||
@@ -148,7 +150,8 @@ func (rs *ResilientSearcher) searchWithProtection(ctx context.Context, engine Se
|
|||||||
return nil, ProxyExecutionMeta{}, ctx.Err()
|
return nil, ProxyExecutionMeta{}, ctx.Err()
|
||||||
}
|
}
|
||||||
cb := rs.cbManager.Get(engine.Name())
|
cb := rs.cbManager.Get(engine.Name())
|
||||||
if !cb.AllowRequest() {
|
engineCtx := WithEngine(ctx, engine.Name())
|
||||||
|
if !cb.AllowRequest(engineCtx) {
|
||||||
return nil, ProxyExecutionMeta{}, ErrCircuitOpen
|
return nil, ProxyExecutionMeta{}, ErrCircuitOpen
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +176,7 @@ func (rs *ResilientSearcher) searchWithProtection(ctx context.Context, engine Se
|
|||||||
attemptQuery.ProxyURL = ""
|
attemptQuery.ProxyURL = ""
|
||||||
attemptMeta.Used = "direct"
|
attemptMeta.Used = "direct"
|
||||||
case ProxyModeTagPool:
|
case ProxyModeTagPool:
|
||||||
proxyURL = rs.selectProxyForQuery(policy, q)
|
proxyURL = rs.selectProxyForQuery(policy, q, engineCtx)
|
||||||
if proxyURL == "" {
|
if proxyURL == "" {
|
||||||
return nil, fmt.Errorf("%w: no healthy proxy available for tag %q", ErrProxyUnavailable, policy.Tag)
|
return nil, fmt.Errorf("%w: no healthy proxy available for tag %q", ErrProxyUnavailable, policy.Tag)
|
||||||
}
|
}
|
||||||
@@ -193,7 +196,7 @@ func (rs *ResilientSearcher) searchWithProtection(ctx context.Context, engine Se
|
|||||||
}
|
}
|
||||||
|
|
||||||
if reportToRegistry {
|
if reportToRegistry {
|
||||||
rs.reportProxyAttempt(proxyURL, err)
|
rs.reportProxyAttempt(engineCtx, proxyURL, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return results, err
|
return results, err
|
||||||
@@ -201,12 +204,12 @@ func (rs *ResilientSearcher) searchWithProtection(ctx context.Context, engine Se
|
|||||||
|
|
||||||
if result.Err != nil {
|
if result.Err != nil {
|
||||||
if !errors.Is(result.Err, ErrProxyUnavailable) {
|
if !errors.Is(result.Err, ErrProxyUnavailable) {
|
||||||
cb.RecordFailure()
|
cb.RecordFailure(engineCtx)
|
||||||
}
|
}
|
||||||
return nil, attemptMeta, result.Err
|
return nil, attemptMeta, result.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
cb.RecordSuccess()
|
cb.RecordSuccess(engineCtx)
|
||||||
return result.Results, attemptMeta, nil
|
return result.Results, attemptMeta, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,8 +228,9 @@ func (rs *ResilientSearcher) SearchAllParallel(ctx context.Context, q Query, eng
|
|||||||
if !engine.IsInitialized() {
|
if !engine.IsInitialized() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !rs.cbManager.Get(engine.Name()).AllowRequest() {
|
engineCtx := WithEngine(ctx, engine.Name())
|
||||||
logrus.Infof("[Resilient] Skipping %s in megasearch (circuit open)", engine.Name())
|
if !rs.cbManager.Get(engine.Name()).AllowRequest(engineCtx) {
|
||||||
|
WithRequest(engineCtx).Debug("Skipping engine in megasearch: circuit open")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,8 +272,9 @@ func (rs *ResilientSearcher) SearchAllImageParallel(ctx context.Context, q Query
|
|||||||
if !engine.IsInitialized() {
|
if !engine.IsInitialized() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !rs.cbManager.Get(engine.Name()).AllowRequest() {
|
engineCtx := WithEngine(ctx, engine.Name())
|
||||||
logrus.Infof("[Resilient] Skipping %s in megaimage (circuit open)", engine.Name())
|
if !rs.cbManager.Get(engine.Name()).AllowRequest(engineCtx) {
|
||||||
|
WithRequest(engineCtx).Debug("Skipping engine in megaimage: circuit open")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,27 +429,27 @@ func (rs *ResilientSearcher) effectivePolicyForQuery(engineName string, q Query)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *ResilientSearcher) selectProxyForTag(tag string) string {
|
func (rs *ResilientSearcher) selectProxyForTag(ctx context.Context, tag string) string {
|
||||||
if rs.proxyRegistry == nil {
|
if rs.proxyRegistry == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return rs.proxyRegistry.NextByTag(tag)
|
return rs.proxyRegistry.NextByTagWithContext(ctx, tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *ResilientSearcher) reportProxyAttempt(proxyURL string, err error) {
|
func (rs *ResilientSearcher) reportProxyAttempt(ctx context.Context, proxyURL string, err error) {
|
||||||
if rs.proxyRegistry == nil || proxyURL == "" {
|
if rs.proxyRegistry == nil || proxyURL == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rs.proxyRegistry.ReportFailure(proxyURL)
|
rs.proxyRegistry.ReportFailure(ctx, proxyURL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rs.proxyRegistry.ReportSuccess(proxyURL)
|
rs.proxyRegistry.ReportSuccess(ctx, proxyURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *ResilientSearcher) selectProxyForQuery(policy ProxyPolicy, q Query) string {
|
func (rs *ResilientSearcher) selectProxyForQuery(policy ProxyPolicy, q Query, ctx context.Context) string {
|
||||||
if policy.Mode != ProxyModeTagPool {
|
if policy.Mode != ProxyModeTagPool {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -453,7 +458,7 @@ func (rs *ResilientSearcher) selectProxyForQuery(policy ProxyPolicy, q Query) st
|
|||||||
return global
|
return global
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rs.selectProxyForTag(policy.Tag)
|
return rs.selectProxyForTag(ctx, policy.Tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrAllEnginesFailed = fmt.Errorf("all search engines failed")
|
var ErrAllEnginesFailed = fmt.Errorf("all search engines failed")
|
||||||
|
|||||||
+12
-11
@@ -38,7 +38,8 @@ type RetryResult struct {
|
|||||||
// RetryableSearch executes searchFn with exponential backoff retries.
|
// RetryableSearch executes searchFn with exponential backoff retries.
|
||||||
// CAPTCHA, parser, engine-internal, and proxy-unavailable errors are not retried.
|
// CAPTCHA, parser, engine-internal, and proxy-unavailable errors are not retried.
|
||||||
func RetryableSearch(ctx context.Context, cfg RetryConfig, engineName string, searchFn func(context.Context) ([]SearchResult, error)) RetryResult {
|
func RetryableSearch(ctx context.Context, cfg RetryConfig, engineName string, searchFn func(context.Context) ([]SearchResult, error)) RetryResult {
|
||||||
ctx = EnsureContext(ctx)
|
ctx = WithEngine(EnsureContext(ctx), engineName)
|
||||||
|
logger := WithRequest(ctx)
|
||||||
if cfg.BackoffFactor <= 0 {
|
if cfg.BackoffFactor <= 0 {
|
||||||
cfg.BackoffFactor = 2.0
|
cfg.BackoffFactor = 2.0
|
||||||
}
|
}
|
||||||
@@ -55,7 +56,10 @@ func RetryableSearch(ctx context.Context, cfg RetryConfig, engineName string, se
|
|||||||
|
|
||||||
if attempt > 0 {
|
if attempt > 0 {
|
||||||
backoff := calculateBackoff(cfg, attempt)
|
backoff := calculateBackoff(cfg, attempt)
|
||||||
logrus.Warnf("[%s] Retry attempt %d/%d after %s", engineName, attempt, cfg.MaxRetries, backoff)
|
logger.WithFields(logrus.Fields{
|
||||||
|
"attempt": attempt,
|
||||||
|
"backoff": backoff.String(),
|
||||||
|
}).Warnf("Retry %d/%d after %s", attempt, cfg.MaxRetries, backoff)
|
||||||
if err := SleepContext(ctx, backoff); err != nil {
|
if err := SleepContext(ctx, backoff); err != nil {
|
||||||
return RetryResult{
|
return RetryResult{
|
||||||
Err: err,
|
Err: err,
|
||||||
@@ -67,9 +71,6 @@ func RetryableSearch(ctx context.Context, cfg RetryConfig, engineName string, se
|
|||||||
|
|
||||||
results, err := searchFn(ctx)
|
results, err := searchFn(ctx)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if attempt > 0 {
|
|
||||||
logrus.Infof("[%s] Succeeded on retry attempt %d", engineName, attempt)
|
|
||||||
}
|
|
||||||
return RetryResult{
|
return RetryResult{
|
||||||
Results: results,
|
Results: results,
|
||||||
Attempts: attempt + 1,
|
Attempts: attempt + 1,
|
||||||
@@ -79,7 +80,7 @@ func RetryableSearch(ctx context.Context, cfg RetryConfig, engineName string, se
|
|||||||
|
|
||||||
lastErr = err
|
lastErr = err
|
||||||
if errors.Is(err, ErrCaptcha) {
|
if errors.Is(err, ErrCaptcha) {
|
||||||
logrus.Warnf("[%s] CAPTCHA detected, skipping retries", engineName)
|
logger.Warn("CAPTCHA detected, skipping retries")
|
||||||
return RetryResult{
|
return RetryResult{
|
||||||
Err: err,
|
Err: err,
|
||||||
Attempts: attempt + 1,
|
Attempts: attempt + 1,
|
||||||
@@ -87,7 +88,7 @@ func RetryableSearch(ctx context.Context, cfg RetryConfig, engineName string, se
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if errors.Is(err, ErrProxyUnavailable) {
|
if errors.Is(err, ErrProxyUnavailable) {
|
||||||
logrus.Warnf("[%s] Proxy unavailable, skipping retries", engineName)
|
logger.Warn("Proxy unavailable, skipping retries")
|
||||||
return RetryResult{
|
return RetryResult{
|
||||||
Err: err,
|
Err: err,
|
||||||
Attempts: attempt + 1,
|
Attempts: attempt + 1,
|
||||||
@@ -95,7 +96,7 @@ func RetryableSearch(ctx context.Context, cfg RetryConfig, engineName string, se
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if errors.Is(err, ErrParser) {
|
if errors.Is(err, ErrParser) {
|
||||||
logrus.Warnf("[%s] Parser failure, skipping retries", engineName)
|
logger.Warn("Parser failure, skipping retries")
|
||||||
return RetryResult{
|
return RetryResult{
|
||||||
Err: err,
|
Err: err,
|
||||||
Attempts: attempt + 1,
|
Attempts: attempt + 1,
|
||||||
@@ -103,7 +104,7 @@ func RetryableSearch(ctx context.Context, cfg RetryConfig, engineName string, se
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if errors.Is(err, ErrEngineInternal) {
|
if errors.Is(err, ErrEngineInternal) {
|
||||||
logrus.Warnf("[%s] Engine panic recovered, skipping retries", engineName)
|
logger.Warn("Engine panic recovered, skipping retries")
|
||||||
return RetryResult{
|
return RetryResult{
|
||||||
Err: err,
|
Err: err,
|
||||||
Attempts: attempt + 1,
|
Attempts: attempt + 1,
|
||||||
@@ -111,7 +112,7 @@ func RetryableSearch(ctx context.Context, cfg RetryConfig, engineName string, se
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if IsContextDone(err) {
|
if IsContextDone(err) {
|
||||||
logrus.Warnf("[%s] Context canceled/deadline exceeded, skipping retries", engineName)
|
logger.Warn("Context canceled/deadline exceeded, skipping retries")
|
||||||
return RetryResult{
|
return RetryResult{
|
||||||
Err: err,
|
Err: err,
|
||||||
Attempts: attempt + 1,
|
Attempts: attempt + 1,
|
||||||
@@ -119,7 +120,7 @@ func RetryableSearch(ctx context.Context, cfg RetryConfig, engineName string, se
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Warnf("[%s] Attempt %d failed: %s", engineName, attempt+1, err)
|
logger.WithField("attempt", attempt+1).Debugf("Attempt %d failed: %s", attempt+1, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return RetryResult{
|
return RetryResult{
|
||||||
|
|||||||
+47
-14
@@ -104,9 +104,13 @@ func NewServerWithOptions(host string, port int, opts ServerOptions, searchEngin
|
|||||||
}
|
}
|
||||||
if opts.CacheTTL > 0 && opts.CacheMaxSize > 0 {
|
if opts.CacheTTL > 0 && opts.CacheMaxSize > 0 {
|
||||||
serv.cache = NewResponseCache(opts.CacheTTL, opts.CacheMaxSize)
|
serv.cache = NewResponseCache(opts.CacheTTL, opts.CacheMaxSize)
|
||||||
logrus.Infof("Response cache enabled: TTL=%s, MaxSize=%d", opts.CacheTTL, opts.CacheMaxSize)
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"cache_ttl": opts.CacheTTL.String(),
|
||||||
|
"cache_max_size": opts.CacheMaxSize,
|
||||||
|
}).Info("Response cache enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.Use(RequestContextMiddleware())
|
||||||
if opts.EnableCORS {
|
if opts.EnableCORS {
|
||||||
app.Use(CORSMiddleware(opts.CORS))
|
app.Use(CORSMiddleware(opts.CORS))
|
||||||
}
|
}
|
||||||
@@ -146,17 +150,25 @@ func NewServerWithOptions(host string, port int, opts ServerOptions, searchEngin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleDedicatedEndpoint(c *fiber.Ctx, engine SearchEngine, isImage bool) error {
|
func (s *Server) handleDedicatedEndpoint(c *fiber.Ctx, engine SearchEngine, isImage bool) error {
|
||||||
|
requestCtx := WithEngine(c.UserContext(), engine.Name())
|
||||||
|
c.SetUserContext(requestCtx)
|
||||||
|
|
||||||
q := Query{}
|
q := Query{}
|
||||||
if err := q.InitFromContext(c); err != nil {
|
if err := q.InitFromContext(c); err != nil {
|
||||||
logrus.Errorf("Error while setting %s query: %s", engine.Name(), err)
|
WithRequest(c.UserContext()).WithError(err).Error("Invalid query parameters")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestCtx = WithQueryHash(c.UserContext(), QueryHashFromQuery(q))
|
||||||
|
c.SetUserContext(requestCtx)
|
||||||
|
|
||||||
action := "search"
|
action := "search"
|
||||||
if isImage {
|
if isImage {
|
||||||
action = "image"
|
action = "image"
|
||||||
}
|
}
|
||||||
logrus.Infof("Starting SERP %s request using %s engine for query: %s", action, engine.Name(), q.Text)
|
WithRequest(requestCtx).
|
||||||
|
WithField("action", action).
|
||||||
|
Debugf("Starting %s request for query: %s", action, q.Text)
|
||||||
|
|
||||||
if hit, err := s.tryServeCacheHit(
|
if hit, err := s.tryServeCacheHit(
|
||||||
c,
|
c,
|
||||||
@@ -177,15 +189,15 @@ func (s *Server) handleDedicatedEndpoint(c *fiber.Ctx, engine SearchEngine, isIm
|
|||||||
|
|
||||||
if isImage {
|
if isImage {
|
||||||
if s.opts.AllowEndpointFallback {
|
if s.opts.AllowEndpointFallback {
|
||||||
res, usedEngine, proxyMeta, searchErr = s.resilient.SearchImageWithFallback(c.UserContext(), engine, q)
|
res, usedEngine, proxyMeta, searchErr = s.resilient.SearchImageWithFallback(requestCtx, engine, q)
|
||||||
} else {
|
} else {
|
||||||
res, usedEngine, proxyMeta, searchErr = s.resilient.SearchImagePrimary(c.UserContext(), engine, q)
|
res, usedEngine, proxyMeta, searchErr = s.resilient.SearchImagePrimary(requestCtx, engine, q)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if s.opts.AllowEndpointFallback {
|
if s.opts.AllowEndpointFallback {
|
||||||
res, usedEngine, proxyMeta, searchErr = s.resilient.SearchWithFallback(c.UserContext(), engine, q)
|
res, usedEngine, proxyMeta, searchErr = s.resilient.SearchWithFallback(requestCtx, engine, q)
|
||||||
} else {
|
} else {
|
||||||
res, usedEngine, proxyMeta, searchErr = s.resilient.SearchPrimary(c.UserContext(), engine, q)
|
res, usedEngine, proxyMeta, searchErr = s.resilient.SearchPrimary(requestCtx, engine, q)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.applyProxyHeaders(c, proxyMeta)
|
s.applyProxyHeaders(c, proxyMeta)
|
||||||
@@ -204,7 +216,10 @@ func (s *Server) handleDedicatedEndpoint(c *fiber.Ctx, engine SearchEngine, isIm
|
|||||||
case errors.Is(searchErr, ErrProxyUnavailable):
|
case errors.Is(searchErr, ErrProxyUnavailable):
|
||||||
errToReturn = fmt.Errorf("%s", searchErr)
|
errToReturn = fmt.Errorf("%s", searchErr)
|
||||||
}
|
}
|
||||||
logrus.Errorf("Error during resilient %s %s: %s", engine.Name(), action, searchErr)
|
WithRequest(requestCtx).
|
||||||
|
WithFields(logrus.Fields{"action": action}).
|
||||||
|
WithError(searchErr).
|
||||||
|
Error("Search failed")
|
||||||
return fiber.NewError(fiber.StatusServiceUnavailable, errToReturn.Error())
|
return fiber.NewError(fiber.StatusServiceUnavailable, errToReturn.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +246,13 @@ func (s *Server) handleDedicatedEndpoint(c *fiber.Ctx, engine SearchEngine, isIm
|
|||||||
c.Set("X-Fallback-Engine", usedEngine)
|
c.Set("X-Fallback-Engine", usedEngine)
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("Successfully completed SERP %s using %s, returned %d results", action, usedEngine, len(res))
|
completionCtx := requestCtx
|
||||||
|
if usedEngine != "" {
|
||||||
|
completionCtx = WithEngine(completionCtx, usedEngine)
|
||||||
|
}
|
||||||
|
WithRequest(completionCtx).
|
||||||
|
WithFields(logrus.Fields{"action": action, "results_count": len(res)}).
|
||||||
|
Info("Search completed")
|
||||||
return c.JSON(res)
|
return c.JSON(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,11 +376,16 @@ func (s *Server) handleMegaImage(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleMegaEndpoint(c *fiber.Ctx, action string, run func(context.Context, Query, []SearchEngine) []MegaSearchResult) error {
|
func (s *Server) handleMegaEndpoint(c *fiber.Ctx, action string, run func(context.Context, Query, []SearchEngine) []MegaSearchResult) error {
|
||||||
|
requestCtx := WithEngine(c.UserContext(), "mega")
|
||||||
|
c.SetUserContext(requestCtx)
|
||||||
|
|
||||||
q := Query{}
|
q := Query{}
|
||||||
if err := q.InitFromContext(c); err != nil {
|
if err := q.InitFromContext(c); err != nil {
|
||||||
logrus.Errorf("Error while setting mega %s query: %s", action, err)
|
WithRequest(c.UserContext()).WithError(err).Error("Invalid query parameters")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
requestCtx = WithQueryHash(c.UserContext(), QueryHashFromQuery(q))
|
||||||
|
c.SetUserContext(requestCtx)
|
||||||
|
|
||||||
enginesToUse := s.resolveEngines(c.Query("engines", ""))
|
enginesToUse := s.resolveEngines(c.Query("engines", ""))
|
||||||
if len(enginesToUse) == 0 {
|
if len(enginesToUse) == 0 {
|
||||||
@@ -372,7 +398,10 @@ func (s *Server) handleMegaEndpoint(c *fiber.Ctx, action string, run func(contex
|
|||||||
}
|
}
|
||||||
engineNamesJoined := strings.Join(engineNames, ",")
|
engineNamesJoined := strings.Join(engineNames, ",")
|
||||||
s.applyProxyHeaders(c, s.resilient.ResolveMegaProxyMeta(q, enginesToUse))
|
s.applyProxyHeaders(c, s.resilient.ResolveMegaProxyMeta(q, enginesToUse))
|
||||||
logrus.Infof("Starting SERP mega %s request using engines: %s for query: %s", action, engineNamesJoined, q.Text)
|
WithRequest(requestCtx).WithFields(logrus.Fields{
|
||||||
|
"action": action,
|
||||||
|
"engines": engineNamesJoined,
|
||||||
|
}).Debugf("Starting mega %s request for query: %s", action, q.Text)
|
||||||
|
|
||||||
cacheHitCandidates := []cacheHitCandidate{
|
cacheHitCandidates := []cacheHitCandidate{
|
||||||
{
|
{
|
||||||
@@ -391,14 +420,18 @@ func (s *Server) handleMegaEndpoint(c *fiber.Ctx, action string, run func(contex
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
results := run(c.UserContext(), q, enginesToUse)
|
results := run(requestCtx, q, enginesToUse)
|
||||||
dedupedResults := s.deduplicateMegaResults(results)
|
dedupedResults := s.deduplicateMegaResults(results)
|
||||||
|
|
||||||
if s.cache != nil {
|
if s.cache != nil {
|
||||||
c.Set("X-Cache", s.cacheMegaResults(action, enginesToUse, q, dedupedResults))
|
c.Set("X-Cache", s.cacheMegaResults(action, enginesToUse, q, dedupedResults))
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("Successfully completed SERP mega %s using %d engines, returned %d deduplicated results", action, len(enginesToUse), len(dedupedResults))
|
WithRequest(requestCtx).WithFields(logrus.Fields{
|
||||||
|
"action": action,
|
||||||
|
"engines_count": len(enginesToUse),
|
||||||
|
"results_count": len(dedupedResults),
|
||||||
|
}).Info("Mega search completed")
|
||||||
return c.JSON(dedupedResults)
|
return c.JSON(dedupedResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,7 +524,7 @@ func (s *Server) tryServeCacheHit(c *fiber.Ctx, candidates ...cacheHitCandidate)
|
|||||||
}
|
}
|
||||||
c.Set("Content-Type", "application/json")
|
c.Set("Content-Type", "application/json")
|
||||||
c.Set("X-Cache", "HIT")
|
c.Set("X-Cache", "HIT")
|
||||||
logrus.Info(candidate.logMessage)
|
WithRequest(c.UserContext()).Debug(candidate.logMessage)
|
||||||
return true, c.Send(cached)
|
return true, c.Send(cached)
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -74,6 +75,36 @@ func requestWithHeader(t *testing.T, s *Server, path string, header string, valu
|
|||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRequestIDHeaderIsEchoedWhenProvided(t *testing.T) {
|
||||||
|
engine := &engineMock{name: "google", initialized: true}
|
||||||
|
srv := NewServerWithOptions("127.0.0.1", 7110, DefaultServerOptions(), engine)
|
||||||
|
|
||||||
|
resp := requestWithHeader(t, srv, "/google/search?text=golang", "X-Request-ID", "foo")
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("expected request to succeed, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
if got := resp.Header.Get("X-Request-ID"); got != "foo" {
|
||||||
|
t.Fatalf("expected X-Request-ID=foo, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestIDHeaderIsGeneratedWhenMissing(t *testing.T) {
|
||||||
|
engine := &engineMock{name: "google", initialized: true}
|
||||||
|
srv := NewServerWithOptions("127.0.0.1", 7111, DefaultServerOptions(), engine)
|
||||||
|
|
||||||
|
resp := request(t, srv, "/google/search?text=golang")
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("expected request to succeed, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
requestID := resp.Header.Get("X-Request-ID")
|
||||||
|
if requestID == "" {
|
||||||
|
t.Fatal("expected non-empty X-Request-ID header")
|
||||||
|
}
|
||||||
|
if _, err := uuid.Parse(requestID); err != nil {
|
||||||
|
t.Fatalf("expected X-Request-ID to be a UUID, got %q (%v)", requestID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestOpenAPISpecEndpoint(t *testing.T) {
|
func TestOpenAPISpecEndpoint(t *testing.T) {
|
||||||
engine := &engineMock{name: "google", initialized: true}
|
engine := &engineMock{name: "google", initialized: true}
|
||||||
srv := NewServerWithOptions("127.0.0.1", 7107, DefaultServerOptions(), engine)
|
srv := NewServerWithOptions("127.0.0.1", 7107, DefaultServerOptions(), engine)
|
||||||
|
|||||||
+13
-3
@@ -174,11 +174,16 @@ func (ddg *DuckDuckGo) parseResults(results rod.Elements, pageNum int) []core.Se
|
|||||||
// Search executes a DuckDuckGo web search and returns normalized search
|
// Search executes a DuckDuckGo web search and returns normalized search
|
||||||
// results. It may return core.ErrCaptcha or core.ErrSearchTimeout.
|
// results. It may return core.ErrCaptcha or core.ErrSearchTimeout.
|
||||||
func (ddg *DuckDuckGo) Search(ctx context.Context, query core.Query) (results []core.SearchResult, err error) {
|
func (ddg *DuckDuckGo) Search(ctx context.Context, query core.Query) (results []core.SearchResult, err error) {
|
||||||
ctx = core.EnsureContext(ctx)
|
ctx = core.WithEngine(core.EnsureContext(ctx), ddg.Name())
|
||||||
|
ctx = core.WithQueryHash(ctx, core.QueryHashFromQuery(query))
|
||||||
|
scoped := *ddg
|
||||||
|
scoped.logger = ddg.logger.WithRequest(ctx)
|
||||||
|
ddg = &scoped
|
||||||
|
|
||||||
ddg.logger.Debug("Starting search, query: %+v", query)
|
ddg.logger.Debug("Starting search, query: %+v", query)
|
||||||
defer func() {
|
defer func() {
|
||||||
if recovered := recover(); recovered != nil {
|
if recovered := recover(); recovered != nil {
|
||||||
err = core.RecoverEnginePanic(ddg.Name(), recovered, ddg.logger)
|
err = core.RecoverEnginePanicWithContext(ctx, ddg.Name(), recovered, ddg.logger)
|
||||||
results = nil
|
results = nil
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -292,7 +297,12 @@ func (ddg *DuckDuckGo) Search(ctx context.Context, query core.Query) (results []
|
|||||||
// SearchImage executes a DuckDuckGo image search and returns normalized image
|
// SearchImage executes a DuckDuckGo image search and returns normalized image
|
||||||
// results. It may return core.ErrCaptcha or core.ErrSearchTimeout.
|
// results. It may return core.ErrCaptcha or core.ErrSearchTimeout.
|
||||||
func (ddg *DuckDuckGo) SearchImage(ctx context.Context, query core.Query) ([]core.SearchResult, error) {
|
func (ddg *DuckDuckGo) SearchImage(ctx context.Context, query core.Query) ([]core.SearchResult, error) {
|
||||||
ctx = core.EnsureContext(ctx)
|
ctx = core.WithEngine(core.EnsureContext(ctx), ddg.Name())
|
||||||
|
ctx = core.WithQueryHash(ctx, core.QueryHashFromQuery(query))
|
||||||
|
scoped := *ddg
|
||||||
|
scoped.logger = ddg.logger.WithRequest(ctx)
|
||||||
|
ddg = &scoped
|
||||||
|
|
||||||
ddg.logger.Debug("Starting image search, query: %+v", query)
|
ddg.logger.Debug("Starting image search, query: %+v", query)
|
||||||
|
|
||||||
searchResults := []core.SearchResult{}
|
searchResults := []core.SearchResult{}
|
||||||
|
|||||||
+13
-3
@@ -173,11 +173,16 @@ func (gogl *Google) acceptCookies(page *rod.Page) {
|
|||||||
// Search executes a Google web search and returns normalized search results.
|
// Search executes a Google web search and returns normalized search results.
|
||||||
// It may return core.ErrCaptcha or core.ErrSearchTimeout.
|
// It may return core.ErrCaptcha or core.ErrSearchTimeout.
|
||||||
func (gogl *Google) Search(ctx context.Context, query core.Query) (results []core.SearchResult, err error) {
|
func (gogl *Google) Search(ctx context.Context, query core.Query) (results []core.SearchResult, err error) {
|
||||||
ctx = core.EnsureContext(ctx)
|
ctx = core.WithEngine(core.EnsureContext(ctx), gogl.Name())
|
||||||
|
ctx = core.WithQueryHash(ctx, core.QueryHashFromQuery(query))
|
||||||
|
scoped := *gogl
|
||||||
|
scoped.logger = gogl.logger.WithRequest(ctx)
|
||||||
|
gogl = &scoped
|
||||||
|
|
||||||
gogl.logger.Debug("Starting search, query: %+v", query)
|
gogl.logger.Debug("Starting search, query: %+v", query)
|
||||||
defer func() {
|
defer func() {
|
||||||
if recovered := recover(); recovered != nil {
|
if recovered := recover(); recovered != nil {
|
||||||
err = core.RecoverEnginePanic(gogl.Name(), recovered, gogl.logger)
|
err = core.RecoverEnginePanicWithContext(ctx, gogl.Name(), recovered, gogl.logger)
|
||||||
results = nil
|
results = nil
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -421,7 +426,12 @@ func (gogl *Google) Search(ctx context.Context, query core.Query) (results []cor
|
|||||||
// SearchImage executes a Google image search and returns normalized image
|
// SearchImage executes a Google image search and returns normalized image
|
||||||
// results. It may return core.ErrCaptcha or core.ErrSearchTimeout.
|
// results. It may return core.ErrCaptcha or core.ErrSearchTimeout.
|
||||||
func (gogl *Google) SearchImage(ctx context.Context, query core.Query) ([]core.SearchResult, error) {
|
func (gogl *Google) SearchImage(ctx context.Context, query core.Query) ([]core.SearchResult, error) {
|
||||||
ctx = core.EnsureContext(ctx)
|
ctx = core.WithEngine(core.EnsureContext(ctx), gogl.Name())
|
||||||
|
ctx = core.WithQueryHash(ctx, core.QueryHashFromQuery(query))
|
||||||
|
scoped := *gogl
|
||||||
|
scoped.logger = gogl.logger.WithRequest(ctx)
|
||||||
|
gogl = &scoped
|
||||||
|
|
||||||
gogl.logger.Debug("Starting image search, query: %+v", query)
|
gogl.logger.Debug("Starting image search, query: %+v", query)
|
||||||
|
|
||||||
searchResultsMap := map[string]core.SearchResult{}
|
searchResultsMap := map[string]core.SearchResult{}
|
||||||
|
|||||||
+14
-5
@@ -2,6 +2,7 @@ package google
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -97,15 +98,19 @@ func googleResultParser(response *http.Response) ([]core.SearchResult, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Tracef("Google search document size: %d", len(doc.Text()))
|
logrus.WithField("document_size", len(doc.Text())).Trace(
|
||||||
|
fmt.Sprintf("Google search document size: %d", len(doc.Text())),
|
||||||
|
)
|
||||||
return core.DeduplicateResults(results), err
|
return core.DeduplicateResults(results), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func Search(ctx context.Context, query core.Query) (results []core.SearchResult, err error) {
|
func Search(ctx context.Context, query core.Query) (results []core.SearchResult, err error) {
|
||||||
ctx = core.EnsureContext(ctx)
|
ctx = core.EnsureContext(ctx)
|
||||||
|
ctx = core.WithEngine(ctx, "google")
|
||||||
|
ctx = core.WithQueryHash(ctx, core.QueryHashFromQuery(query))
|
||||||
defer func() {
|
defer func() {
|
||||||
if recovered := recover(); recovered != nil {
|
if recovered := recover(); recovered != nil {
|
||||||
err = core.RecoverEnginePanic("google", recovered, nil)
|
err = core.RecoverEnginePanicWithContext(ctx, "google", recovered, nil)
|
||||||
results = nil
|
results = nil
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -114,14 +119,16 @@ func Search(ctx context.Context, query core.Query) (results []core.SearchResult,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
logrus.Debugf("Google URL built: %s", googleURL)
|
core.WithRequest(ctx).WithField("url", googleURL).Debug(fmt.Sprintf("Google URL built: %s", googleURL))
|
||||||
|
|
||||||
res, err := googleRequest(ctx, googleURL, query)
|
res, err := googleRequest(ctx, googleURL, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer core.DrainAndCloseResponse(res)
|
defer core.DrainAndCloseResponse(res)
|
||||||
logrus.Debugf("Google Raw response: code=%d", res.StatusCode)
|
core.WithRequest(ctx).WithField("status_code", res.StatusCode).Debug(
|
||||||
|
fmt.Sprintf("Google Raw response: code=%d", res.StatusCode),
|
||||||
|
)
|
||||||
|
|
||||||
parsedResults, err := googleResultParser(res)
|
parsedResults, err := googleResultParser(res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -133,7 +140,9 @@ func Search(ctx context.Context, query core.Query) (results []core.SearchResult,
|
|||||||
parsedResults[i].Rank = query.Start + i + 1
|
parsedResults[i].Rank = query.Start + i + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logrus.Debugf("Google Raw results : %v", parsedResults)
|
core.WithRequest(ctx).WithField("results_count", len(parsedResults)).Debug(
|
||||||
|
fmt.Sprintf("Google Raw results : %v", parsedResults),
|
||||||
|
)
|
||||||
|
|
||||||
return parsedResults, nil
|
return parsedResults, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -237,7 +237,7 @@ func BuildURL(q core.Query) (string, error) {
|
|||||||
text += " filetype:" + q.Filetype
|
text += " filetype:" + q.Filetype
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Tracef("Query text: %s", text)
|
logrus.WithField("query_hash", core.QueryHash(text)).Trace(fmt.Sprintf("Query text: %s", text))
|
||||||
params.Add("q", text)
|
params.Add("q", text)
|
||||||
params.Add("oq", text)
|
params.Add("oq", text)
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-3
@@ -127,11 +127,16 @@ func (yand *Yandex) parseResults(results rod.Elements, pageNum int) []core.Searc
|
|||||||
// Search executes a Yandex web search and returns normalized search results.
|
// Search executes a Yandex web search and returns normalized search results.
|
||||||
// It may return core.ErrCaptcha or core.ErrSearchTimeout.
|
// It may return core.ErrCaptcha or core.ErrSearchTimeout.
|
||||||
func (yand *Yandex) Search(ctx context.Context, query core.Query) (results []core.SearchResult, err error) {
|
func (yand *Yandex) Search(ctx context.Context, query core.Query) (results []core.SearchResult, err error) {
|
||||||
ctx = core.EnsureContext(ctx)
|
ctx = core.WithEngine(core.EnsureContext(ctx), yand.Name())
|
||||||
|
ctx = core.WithQueryHash(ctx, core.QueryHashFromQuery(query))
|
||||||
|
scoped := *yand
|
||||||
|
scoped.logger = yand.logger.WithRequest(ctx)
|
||||||
|
yand = &scoped
|
||||||
|
|
||||||
yand.logger.Debug("Starting search, query: %+v", query)
|
yand.logger.Debug("Starting search, query: %+v", query)
|
||||||
defer func() {
|
defer func() {
|
||||||
if recovered := recover(); recovered != nil {
|
if recovered := recover(); recovered != nil {
|
||||||
err = core.RecoverEnginePanic(yand.Name(), recovered, yand.logger)
|
err = core.RecoverEnginePanicWithContext(ctx, yand.Name(), recovered, yand.logger)
|
||||||
results = nil
|
results = nil
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -221,7 +226,12 @@ func (yand *Yandex) Search(ctx context.Context, query core.Query) (results []cor
|
|||||||
// SearchImage executes a Yandex image search and returns normalized image
|
// SearchImage executes a Yandex image search and returns normalized image
|
||||||
// results. It may return core.ErrCaptcha or core.ErrSearchTimeout.
|
// results. It may return core.ErrCaptcha or core.ErrSearchTimeout.
|
||||||
func (yand *Yandex) SearchImage(ctx context.Context, query core.Query) ([]core.SearchResult, error) {
|
func (yand *Yandex) SearchImage(ctx context.Context, query core.Query) ([]core.SearchResult, error) {
|
||||||
ctx = core.EnsureContext(ctx)
|
ctx = core.WithEngine(core.EnsureContext(ctx), yand.Name())
|
||||||
|
ctx = core.WithQueryHash(ctx, core.QueryHashFromQuery(query))
|
||||||
|
scoped := *yand
|
||||||
|
scoped.logger = yand.logger.WithRequest(ctx)
|
||||||
|
yand = &scoped
|
||||||
|
|
||||||
yand.logger.Debug("Starting image search, query: %+v", query)
|
yand.logger.Debug("Starting image search, query: %+v", query)
|
||||||
|
|
||||||
searchResults := []core.SearchResult{}
|
searchResults := []core.SearchResult{}
|
||||||
|
|||||||
+14
-5
@@ -2,6 +2,7 @@ package yandex
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -85,15 +86,19 @@ func yandexResultParser(response *http.Response) ([]core.SearchResult, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Tracef("Yandex search document size: %d", len(doc.Text()))
|
logrus.WithField("document_size", len(doc.Text())).Trace(
|
||||||
|
fmt.Sprintf("Yandex search document size: %d", len(doc.Text())),
|
||||||
|
)
|
||||||
return core.DeduplicateResults(results), err
|
return core.DeduplicateResults(results), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func Search(ctx context.Context, query core.Query) (results []core.SearchResult, err error) {
|
func Search(ctx context.Context, query core.Query) (results []core.SearchResult, err error) {
|
||||||
ctx = core.EnsureContext(ctx)
|
ctx = core.EnsureContext(ctx)
|
||||||
|
ctx = core.WithEngine(ctx, "yandex")
|
||||||
|
ctx = core.WithQueryHash(ctx, core.QueryHashFromQuery(query))
|
||||||
defer func() {
|
defer func() {
|
||||||
if recovered := recover(); recovered != nil {
|
if recovered := recover(); recovered != nil {
|
||||||
err = core.RecoverEnginePanic("yandex", recovered, nil)
|
err = core.RecoverEnginePanicWithContext(ctx, "yandex", recovered, nil)
|
||||||
results = nil
|
results = nil
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -107,14 +112,16 @@ func Search(ctx context.Context, query core.Query) (results []core.SearchResult,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
logrus.Debugf("Yandex URL built: %s", googleURL)
|
core.WithRequest(ctx).WithField("url", googleURL).Debug(fmt.Sprintf("Yandex URL built: %s", googleURL))
|
||||||
|
|
||||||
res, err := yandexRequest(ctx, googleURL, query)
|
res, err := yandexRequest(ctx, googleURL, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer core.DrainAndCloseResponse(res)
|
defer core.DrainAndCloseResponse(res)
|
||||||
logrus.Debugf("Yandex Raw response: code=%d", res.StatusCode)
|
core.WithRequest(ctx).WithField("status_code", res.StatusCode).Debug(
|
||||||
|
fmt.Sprintf("Yandex Raw response: code=%d", res.StatusCode),
|
||||||
|
)
|
||||||
|
|
||||||
parsedResults, err := yandexResultParser(res)
|
parsedResults, err := yandexResultParser(res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -133,7 +140,9 @@ func Search(ctx context.Context, query core.Query) (results []core.SearchResult,
|
|||||||
parsedResults[i].Rank = query.Start + i + 1
|
parsedResults[i].Rank = query.Start + i + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logrus.Debugf("Yandex Raw results : %v", parsedResults)
|
core.WithRequest(ctx).WithField("results_count", len(parsedResults)).Debug(
|
||||||
|
fmt.Sprintf("Yandex Raw results : %v", parsedResults),
|
||||||
|
)
|
||||||
|
|
||||||
return parsedResults, nil
|
return parsedResults, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user