feat: expose provider fetch metrics
This commit is contained in:
parent
38f4504aa8
commit
162fd4b7b8
@ -46,6 +46,11 @@ Proxy Pool 用 Controller 协调这些变化,并让 Gateway 数据面只消费
|
|||||||
- **Drain 指标**:Controller 暴露 `proxy_pool_controller_drain_candidates_total` 与
|
- **Drain 指标**:Controller 暴露 `proxy_pool_controller_drain_candidates_total` 与
|
||||||
`proxy_pool_controller_drains_started_total`;`reason` 仅有 `unhealthy` 与
|
`proxy_pool_controller_drains_started_total`;`reason` 仅有 `unhealthy` 与
|
||||||
`upstream_disabled`,不包含 Proxy、Worker、Upstream、会话或地址。
|
`upstream_disabled`,不包含 Proxy、Worker、Upstream、会话或地址。
|
||||||
|
- **Provider 指标**:Controller 暴露
|
||||||
|
`proxy_pool_controller_provider_fetch_results_total{class}`、
|
||||||
|
`proxy_pool_controller_provider_valid_candidates_total` 与
|
||||||
|
`proxy_pool_controller_provider_new_proxies_total`;`class` 仅有 `valid`、
|
||||||
|
`empty`、`duplicate_only`、`error`,不包含 Upstream、Proxy 或错误文本标签。
|
||||||
- **PostgreSQL 管理面**:持久化配置版本、Upstream/Routing 管理状态、Admin
|
- **PostgreSQL 管理面**:持久化配置版本、Upstream/Routing 管理状态、Admin
|
||||||
审计与 Outbox;不保存 Proxy 明细或逐次提取记录。
|
审计与 Outbox;不保存 Proxy 明细或逐次提取记录。
|
||||||
- **Gateway 组件**:HTTP 正向代理、HTTPS CONNECT、双向 Tunnel、重试、超时、
|
- **Gateway 组件**:HTTP 正向代理、HTTPS CONNECT、双向 Tunnel、重试、超时、
|
||||||
|
|||||||
@ -205,8 +205,8 @@ Distribution/Admin 服务构造、错误合并和资源关闭。生产 Provider
|
|||||||
停机;Admin disable 会取消 Runtime,reload 在提交前预检并在发布后替换运行实例。
|
停机;Admin disable 会取消 Runtime,reload 在提交前预检并在发布后替换运行实例。
|
||||||
组合 fixture 已验证隔离 Redis namespace 下的选主、Provider HTTP 调用、模板解析
|
组合 fixture 已验证隔离 Redis namespace 下的选主、Provider HTTP 调用、模板解析
|
||||||
和活动池写入。Controller Metrics 独立入口现已提供 `/livez`、`/readyz` 与基础
|
和活动池写入。Controller Metrics 独立入口现已提供 `/livez`、`/readyz` 与基础
|
||||||
Prometheus 运行时指标,三监听器隔离已通过测试;业务指标仍待
|
Prometheus 运行时指标,三监听器隔离已通过测试;Checker、Gateway、Drain 与
|
||||||
实现。双存储 bootstrap 已通过 PostgreSQL 18 + Redis 8.2 组合 fixture,覆盖
|
Provider 业务指标已接入,提取、容量指标和密钥安全的结构化日志仍待实现。双存储 bootstrap 已通过 PostgreSQL 18 + Redis 8.2 组合 fixture,覆盖
|
||||||
迁移、启动配置提交、Readiness、Admin Status 和 Metrics 探针。
|
迁移、启动配置提交、Readiness、Admin Status 和 Metrics 探针。
|
||||||
|
|
||||||
WorkerControlPlane 现已接入 Controller 生命周期:Register、ACK 和 Runtime
|
WorkerControlPlane 现已接入 Controller 生命周期:Register、ACK 和 Runtime
|
||||||
@ -351,7 +351,10 @@ Supervisor 也改为同时服从静态配置与管理态,消除两条启停消
|
|||||||
`BASIC`、`EGRESS`、`TARGET` 级别及 `accepted`、`rejected` 结果。Gateway 还暴露
|
`BASIC`、`EGRESS`、`TARGET` 级别及 `accepted`、`rejected` 结果。Gateway 还暴露
|
||||||
`proxy_pool_gateway_outcomes_total{stage,result}` 和
|
`proxy_pool_gateway_outcomes_total{stage,result}` 和
|
||||||
`proxy_pool_gateway_outcome_queue_dropped_total`,其中阶段和结果均为固定枚举。Provider、
|
`proxy_pool_gateway_outcome_queue_dropped_total`,其中阶段和结果均为固定枚举。Provider、
|
||||||
提取、容量指标与密钥安全的结构化日志仍待补齐。
|
拉取已暴露 `proxy_pool_controller_provider_fetch_results_total{class}`、
|
||||||
|
`proxy_pool_controller_provider_valid_candidates_total` 与
|
||||||
|
`proxy_pool_controller_provider_new_proxies_total`;`class` 仅允许 `valid`、`empty`、
|
||||||
|
`duplicate_only`、`error`。Provider 指标已完成,提取、容量指标与密钥安全的结构化日志仍待补齐。
|
||||||
|
|
||||||
## Task 14: Documentation, Examples, and Diagrams
|
## Task 14: Documentation, Examples, and Diagrams
|
||||||
|
|
||||||
|
|||||||
@ -87,5 +87,5 @@
|
|||||||
| OPS-001 | 配置校验后构建不可变快照并原子替换 | 8959-8999 | 100k 索引、版本/epoch 与并发 Apply/Acquire 测试 |
|
| OPS-001 | 配置校验后构建不可变快照并原子替换 | 8959-8999 | 100k 索引、版本/epoch 与并发 Apply/Acquire 测试 |
|
||||||
| OPS-002 | 优雅停机停止新请求/Fetch,等待现有流量后超时关闭 | 8981-9000 | Provider Run 收敛与 `Handler.Shutdown` HTTP 排空、Hijacked CONNECT 超时关闭测试 |
|
| OPS-002 | 优雅停机停止新请求/Fetch,等待现有流量后超时关闭 | 8981-9000 | Provider Run 收敛与 `Handler.Shutdown` HTTP 排空、Hijacked CONNECT 超时关闭测试 |
|
||||||
| OPS-003 | PostgreSQL 只保存管理修订、Upstream/Routing 状态、Admin 审计与 Outbox | 当前会话 | ADR-006、`adminstate` 公用契约和六表 Schema 边界测试;真实 PostgreSQL 契约待完成 |
|
| OPS-003 | PostgreSQL 只保存管理修订、Upstream/Routing 状态、Admin 审计与 Outbox | 当前会话 | ADR-006、`adminstate` 公用契约和六表 Schema 边界测试;真实 PostgreSQL 契约待完成 |
|
||||||
| OBS-001 | 指标禁止 Proxy IP、session、Client、完整 URL 高基数标签 | 9001-9029 | Controller Prometheus/探针模块已实现;Checker Collector 暴露固定等级和结果,Gateway Collector 暴露 `proxy_pool_gateway_outcomes_total{stage,result}` 与 `proxy_pool_gateway_outcome_queue_dropped_total`,Drain Collector 暴露 `proxy_pool_controller_drain_candidates_total{reason}` 与 `proxy_pool_controller_drains_started_total{reason}`,其中 reason 固定为 unhealthy/upstream_disabled;注册表测试锁定标签集。Provider、提取和容量业务指标待实现 |
|
| OBS-001 | 指标禁止 Proxy IP、session、Client、完整 URL 高基数标签 | 9001-9029 | Controller Prometheus/探针模块已实现;Checker Collector 暴露固定等级和结果,Gateway Collector 暴露 `proxy_pool_gateway_outcomes_total{stage,result}` 与 `proxy_pool_gateway_outcome_queue_dropped_total`,Drain Collector 暴露 `proxy_pool_controller_drain_candidates_total{reason}` 与 `proxy_pool_controller_drains_started_total{reason}`,其中 reason 固定为 unhealthy/upstream_disabled;Provider Collector 暴露 `proxy_pool_controller_provider_fetch_results_total{class}`、`proxy_pool_controller_provider_valid_candidates_total` 与 `proxy_pool_controller_provider_new_proxies_total`,其中 class 固定为 valid/empty/duplicate_only/error;注册表测试锁定标签集。Provider 指标已完成,提取、容量指标和密钥安全的结构化日志仍待实现 |
|
||||||
| TEST-001 | 覆盖对话中列出的 11 个关键并发与故障场景 | 9030-9082 | 测试清单;Redis 活动池由 Memory/Redis 公用契约覆盖,跨进程故障场景仍按清单推进 |
|
| TEST-001 | 覆盖对话中列出的 11 个关键并发与故障场景 | 9030-9082 | 测试清单;Redis 活动池由 Memory/Redis 公用契约覆盖,跨进程故障场景仍按清单推进 |
|
||||||
|
|||||||
@ -49,6 +49,13 @@ Extract 请求使用哪些 Upstream。
|
|||||||
- Gateway、Distribution、Admin、Metrics 使用独立监听和认证/访问控制。
|
- Gateway、Distribution、Admin、Metrics 使用独立监听和认证/访问控制。
|
||||||
- 非回环监听且无认证、无 CIDR 保护时,严格模式必须拒绝启动。
|
- 非回环监听且无认证、无 CIDR 保护时,严格模式必须拒绝启动。
|
||||||
|
|
||||||
|
## 可观测性边界
|
||||||
|
|
||||||
|
- Provider 拉取结果只按固定 `class` 聚合:`valid`、`empty`、`duplicate_only`、`error`。
|
||||||
|
指标分别记录拉取结果次数、合法候选数和新入池代理数,不按 Upstream、Proxy、IP、错误
|
||||||
|
文本或凭据拆分。
|
||||||
|
- Provider 指标已完成;提取、容量指标和密钥安全的结构化日志仍待实现。
|
||||||
|
|
||||||
## 集群与性能
|
## 集群与性能
|
||||||
|
|
||||||
- 用户补充:高峰可能达到 100,000 请求/秒。
|
- 用户补充:高峰可能达到 100,000 请求/秒。
|
||||||
|
|||||||
@ -237,6 +237,13 @@ func runWithWorkerFactory(
|
|||||||
return fmt.Errorf("%w: build Drain metrics: %w", ErrStartup, drainCollectorErr)
|
return fmt.Errorf("%w: build Drain metrics: %w", ErrStartup, drainCollectorErr)
|
||||||
}
|
}
|
||||||
drainMetrics = drainCollector
|
drainMetrics = drainCollector
|
||||||
|
providerCollector, providerCollectorErr := platformMetrics.NewProviderCollector(prometheus.DefaultRegisterer)
|
||||||
|
if providerCollectorErr != nil {
|
||||||
|
return fmt.Errorf("%w: build Provider metrics: %w", ErrStartup, providerCollectorErr)
|
||||||
|
}
|
||||||
|
if registrar, ok := opened.providerResults.(provider.ResultObserverRegistrar); ok && !nilInterface(registrar) {
|
||||||
|
registrar.AddResultObserver(providerCollector)
|
||||||
|
}
|
||||||
handler, handlerErr := platformMetrics.NewHandler(platformMetrics.Dependencies{
|
handler, handlerErr := platformMetrics.NewHandler(platformMetrics.Dependencies{
|
||||||
Gatherer: prometheus.DefaultGatherer, Readiness: opened.metricsReadiness,
|
Gatherer: prometheus.DefaultGatherer, Readiness: opened.metricsReadiness,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -49,6 +49,18 @@ type ResultRecorder interface {
|
|||||||
Record(Result)
|
Record(Result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResultObserver receives one validated Provider fetch result. Observers must
|
||||||
|
// not retain sensitive response data or block the fetch reconciliation loop.
|
||||||
|
type ResultObserver interface {
|
||||||
|
ObserveProviderResult(Result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResultObserverRegistrar lets bootstrap attach low-cardinality observers to
|
||||||
|
// a shared result recorder without changing the Provider runtime contract.
|
||||||
|
type ResultObserverRegistrar interface {
|
||||||
|
AddResultObserver(ResultObserver)
|
||||||
|
}
|
||||||
|
|
||||||
type Ports struct {
|
type Ports struct {
|
||||||
Adapter ProviderAdapter
|
Adapter ProviderAdapter
|
||||||
Parser Parser
|
Parser Parser
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package provider
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"math"
|
"math"
|
||||||
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"proxy-pool/internal/domain/upstream"
|
"proxy-pool/internal/domain/upstream"
|
||||||
@ -25,9 +26,10 @@ type StatsRetainer interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type StatsRecorder struct {
|
type StatsRecorder struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
maximum int
|
maximum int
|
||||||
byID map[string]Stats
|
byID map[string]Stats
|
||||||
|
observers []ResultObserver
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStatsRecorder(maximum int) (*StatsRecorder, error) {
|
func NewStatsRecorder(maximum int) (*StatsRecorder, error) {
|
||||||
@ -38,14 +40,16 @@ func NewStatsRecorder(maximum int) (*StatsRecorder, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (recorder *StatsRecorder) Record(result Result) {
|
func (recorder *StatsRecorder) Record(result Result) {
|
||||||
if recorder == nil || result.UpstreamID == "" {
|
if recorder == nil || result.UpstreamID == "" || !validResultClass(result.Class) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
recorder.mu.Lock()
|
recorder.mu.Lock()
|
||||||
defer recorder.mu.Unlock()
|
observers := append([]ResultObserver(nil), recorder.observers...)
|
||||||
stats, exists := recorder.byID[result.UpstreamID]
|
stats, exists := recorder.byID[result.UpstreamID]
|
||||||
if !exists {
|
if !exists {
|
||||||
if len(recorder.byID) >= recorder.maximum {
|
if len(recorder.byID) >= recorder.maximum {
|
||||||
|
recorder.mu.Unlock()
|
||||||
|
recorder.observe(observers, result)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
stats.UpstreamID = result.UpstreamID
|
stats.UpstreamID = result.UpstreamID
|
||||||
@ -61,10 +65,50 @@ func (recorder *StatsRecorder) Record(result Result) {
|
|||||||
if stats.FetchErrorCount < math.MaxInt64 {
|
if stats.FetchErrorCount < math.MaxInt64 {
|
||||||
stats.FetchErrorCount++
|
stats.FetchErrorCount++
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
recorder.byID[result.UpstreamID] = stats
|
recorder.byID[result.UpstreamID] = stats
|
||||||
|
recorder.mu.Unlock()
|
||||||
|
recorder.observe(observers, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddResultObserver appends one process-local observer. Duplicate instances
|
||||||
|
// are ignored so repeated bootstrap assembly cannot double-count metrics.
|
||||||
|
func (recorder *StatsRecorder) AddResultObserver(observer ResultObserver) {
|
||||||
|
if recorder == nil || observer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
recorder.mu.Lock()
|
||||||
|
defer recorder.mu.Unlock()
|
||||||
|
for _, existing := range recorder.observers {
|
||||||
|
if sameResultObserver(existing, observer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recorder.observers = append(recorder.observers, observer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (recorder *StatsRecorder) observe(observers []ResultObserver, result Result) {
|
||||||
|
for _, observer := range observers {
|
||||||
|
observer.ObserveProviderResult(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validResultClass(class upstream.FetchClass) bool {
|
||||||
|
switch class {
|
||||||
|
case upstream.FetchValid, upstream.FetchEmpty, upstream.FetchDuplicateOnly, upstream.FetchError:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sameResultObserver(left, right ResultObserver) bool {
|
||||||
|
leftValue := reflect.ValueOf(left)
|
||||||
|
rightValue := reflect.ValueOf(right)
|
||||||
|
if !leftValue.IsValid() || !rightValue.IsValid() || leftValue.Type() != rightValue.Type() || !leftValue.Type().Comparable() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return leftValue.Interface() == rightValue.Interface()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (recorder *StatsRecorder) ReadProviderStats(upstreamIDs []string) []Stats {
|
func (recorder *StatsRecorder) ReadProviderStats(upstreamIDs []string) []Stats {
|
||||||
@ -101,3 +145,5 @@ func (recorder *StatsRecorder) RetainProviderStats(upstreamIDs []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ ResultObserverRegistrar = (*StatsRecorder)(nil)
|
||||||
|
|||||||
@ -81,3 +81,30 @@ func TestStatsRecorderRetainsConfiguredProvidersAndReusesCapacity(t *testing.T)
|
|||||||
t.Fatalf("stats after retention = %+v", stats)
|
t.Fatalf("stats after retention = %+v", stats)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStatsRecorderObservesValidatedResultsOutsideBoundedStatusStorage(t *testing.T) {
|
||||||
|
recorder, err := NewStatsRecorder(1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewStatsRecorder() = %v", err)
|
||||||
|
}
|
||||||
|
observer := &providerResultObserverStub{}
|
||||||
|
recorder.AddResultObserver(observer)
|
||||||
|
recorder.AddResultObserver(observer)
|
||||||
|
recorder.Record(Result{UpstreamID: "provider-a", Class: upstream.FetchEmpty})
|
||||||
|
recorder.Record(Result{UpstreamID: "provider-b", Class: upstream.FetchValid, ValidCount: 3, NewCount: 2})
|
||||||
|
recorder.Record(Result{UpstreamID: "provider-c", Class: "invalid"})
|
||||||
|
|
||||||
|
if len(observer.results) != 2 || observer.results[0].UpstreamID != "provider-a" || observer.results[1].UpstreamID != "provider-b" {
|
||||||
|
t.Fatalf("observed results = %+v", observer.results)
|
||||||
|
}
|
||||||
|
stats := recorder.ReadProviderStats([]string{"provider-a", "provider-b"})
|
||||||
|
if stats[0].ConsecutiveEmptyFetch != 1 || stats[1].ConsecutiveEmptyFetch != 0 || stats[1].FetchErrorCount != 0 {
|
||||||
|
t.Fatalf("bounded status stats = %+v", stats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type providerResultObserverStub struct{ results []Result }
|
||||||
|
|
||||||
|
func (observer *providerResultObserverStub) ObserveProviderResult(result Result) {
|
||||||
|
observer.results = append(observer.results, result)
|
||||||
|
}
|
||||||
|
|||||||
68
internal/platform/metrics/provider.go
Normal file
68
internal/platform/metrics/provider.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
|
controllerProvider "proxy-pool/internal/controller/provider"
|
||||||
|
"proxy-pool/internal/domain/upstream"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProviderCollector exposes bounded Provider fetch outcomes. It never uses
|
||||||
|
// upstream names, proxy addresses, response bodies or error strings as labels.
|
||||||
|
type ProviderCollector struct {
|
||||||
|
results *prometheus.CounterVec
|
||||||
|
candidates prometheus.Counter
|
||||||
|
inserted prometheus.Counter
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ controllerProvider.ResultObserver = (*ProviderCollector)(nil)
|
||||||
|
|
||||||
|
func NewProviderCollector(registerer prometheus.Registerer) (*ProviderCollector, error) {
|
||||||
|
if registerer == nil {
|
||||||
|
return nil, ErrInvalidDependencies
|
||||||
|
}
|
||||||
|
results, err := registerCounterVec(registerer, prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Namespace: "proxy_pool", Subsystem: "controller", Name: "provider_fetch_results_total",
|
||||||
|
Help: "Number of completed Provider fetch reconciliations by fixed result class.",
|
||||||
|
}, []string{"class"}))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
candidates, err := registerCounter(registerer, prometheus.NewCounter(prometheus.CounterOpts{
|
||||||
|
Namespace: "proxy_pool", Subsystem: "controller", Name: "provider_valid_candidates_total",
|
||||||
|
Help: "Number of valid Provider proxy candidates parsed before deduplication.",
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inserted, err := registerCounter(registerer, prometheus.NewCounter(prometheus.CounterOpts{
|
||||||
|
Namespace: "proxy_pool", Subsystem: "controller", Name: "provider_new_proxies_total",
|
||||||
|
Help: "Number of Provider proxy candidates inserted into the activity pool.",
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &ProviderCollector{results: results, candidates: candidates, inserted: inserted}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (collector *ProviderCollector) ObserveProviderResult(result controllerProvider.Result) {
|
||||||
|
if collector == nil || collector.results == nil || !validProviderFetchClass(result.Class) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
collector.results.WithLabelValues(string(result.Class)).Inc()
|
||||||
|
if result.ValidCount > 0 && collector.candidates != nil {
|
||||||
|
collector.candidates.Add(float64(result.ValidCount))
|
||||||
|
}
|
||||||
|
if result.NewCount > 0 && collector.inserted != nil {
|
||||||
|
collector.inserted.Add(float64(result.NewCount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validProviderFetchClass(class upstream.FetchClass) bool {
|
||||||
|
switch class {
|
||||||
|
case upstream.FetchValid, upstream.FetchEmpty, upstream.FetchDuplicateOnly, upstream.FetchError:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
69
internal/platform/metrics/provider_test.go
Normal file
69
internal/platform/metrics/provider_test.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
|
controllerProvider "proxy-pool/internal/controller/provider"
|
||||||
|
"proxy-pool/internal/domain/upstream"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProviderCollectorRecordsOnlyFixedResultClasses(t *testing.T) {
|
||||||
|
registry := prometheus.NewRegistry()
|
||||||
|
collector, err := NewProviderCollector(registry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewProviderCollector() error = %v", err)
|
||||||
|
}
|
||||||
|
collector.ObserveProviderResult(controllerProvider.Result{Class: upstream.FetchValid, ValidCount: 5, NewCount: 2})
|
||||||
|
collector.ObserveProviderResult(controllerProvider.Result{Class: upstream.FetchDuplicateOnly, ValidCount: 3})
|
||||||
|
collector.ObserveProviderResult(controllerProvider.Result{Class: upstream.FetchEmpty})
|
||||||
|
collector.ObserveProviderResult(controllerProvider.Result{Class: upstream.FetchError})
|
||||||
|
collector.ObserveProviderResult(controllerProvider.Result{Class: "invalid", ValidCount: 9, NewCount: 9})
|
||||||
|
|
||||||
|
assertMetricValue(t, registry, "proxy_pool_controller_provider_fetch_results_total", map[string]string{"class": "valid"}, 1)
|
||||||
|
assertMetricValue(t, registry, "proxy_pool_controller_provider_fetch_results_total", map[string]string{"class": "duplicate_only"}, 1)
|
||||||
|
assertMetricValue(t, registry, "proxy_pool_controller_provider_fetch_results_total", map[string]string{"class": "empty"}, 1)
|
||||||
|
assertMetricValue(t, registry, "proxy_pool_controller_provider_fetch_results_total", map[string]string{"class": "error"}, 1)
|
||||||
|
assertMetricValue(t, registry, "proxy_pool_controller_provider_valid_candidates_total", nil, 8)
|
||||||
|
assertMetricValue(t, registry, "proxy_pool_controller_provider_new_proxies_total", nil, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewProviderCollectorReusesRegisteredCollectors(t *testing.T) {
|
||||||
|
registry := prometheus.NewRegistry()
|
||||||
|
first, err := NewProviderCollector(registry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("first NewProviderCollector() error = %v", err)
|
||||||
|
}
|
||||||
|
second, err := NewProviderCollector(registry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("second NewProviderCollector() error = %v", err)
|
||||||
|
}
|
||||||
|
first.ObserveProviderResult(controllerProvider.Result{Class: upstream.FetchValid, ValidCount: 1, NewCount: 1})
|
||||||
|
second.ObserveProviderResult(controllerProvider.Result{Class: upstream.FetchValid, ValidCount: 2, NewCount: 2})
|
||||||
|
assertMetricValue(t, registry, "proxy_pool_controller_provider_fetch_results_total", map[string]string{"class": "valid"}, 2)
|
||||||
|
assertMetricValue(t, registry, "proxy_pool_controller_provider_valid_candidates_total", nil, 3)
|
||||||
|
assertMetricValue(t, registry, "proxy_pool_controller_provider_new_proxies_total", nil, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProviderCollectorObservesSharedStatsRecorder(t *testing.T) {
|
||||||
|
registry := prometheus.NewRegistry()
|
||||||
|
collector, err := NewProviderCollector(registry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewProviderCollector() error = %v", err)
|
||||||
|
}
|
||||||
|
recorder, err := controllerProvider.NewStatsRecorder(1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewStatsRecorder() error = %v", err)
|
||||||
|
}
|
||||||
|
recorder.AddResultObserver(collector)
|
||||||
|
recorder.Record(controllerProvider.Result{UpstreamID: "provider-a", Class: upstream.FetchValid, ValidCount: 2, NewCount: 1})
|
||||||
|
// The bounded Admin-status map has no room for provider-b, but metrics must
|
||||||
|
// retain the process-wide fetch observation.
|
||||||
|
recorder.Record(controllerProvider.Result{UpstreamID: "provider-b", Class: upstream.FetchEmpty})
|
||||||
|
|
||||||
|
assertMetricValue(t, registry, "proxy_pool_controller_provider_fetch_results_total", map[string]string{"class": "valid"}, 1)
|
||||||
|
assertMetricValue(t, registry, "proxy_pool_controller_provider_fetch_results_total", map[string]string{"class": "empty"}, 1)
|
||||||
|
assertMetricValue(t, registry, "proxy_pool_controller_provider_valid_candidates_total", nil, 2)
|
||||||
|
assertMetricValue(t, registry, "proxy_pool_controller_provider_new_proxies_total", nil, 1)
|
||||||
|
}
|
||||||
@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
## 2026-08-02
|
## 2026-08-02
|
||||||
|
|
||||||
|
- Provider 拉取可观测性已接入公用 `ResultObserver`:Controller 暴露
|
||||||
|
`proxy_pool_controller_provider_fetch_results_total{class}`、
|
||||||
|
`proxy_pool_controller_provider_valid_candidates_total` 与
|
||||||
|
`proxy_pool_controller_provider_new_proxies_total`。`class` 仅允许 `valid`、`empty`、
|
||||||
|
`duplicate_only`、`error`;指标不包含 Upstream、Proxy、IP、错误文本或凭据标签。
|
||||||
|
Provider 指标已完成,提取、容量指标和密钥安全的结构化日志仍待实现。
|
||||||
- 已实现管理态驱动的即时完整快照刷新:`SnapshotRefreshBroker` 以每个 Worker 流一格的
|
- 已实现管理态驱动的即时完整快照刷新:`SnapshotRefreshBroker` 以每个 Worker 流一格的
|
||||||
非阻塞通道广播,连续变更自动合并。Admin 成功提交 Upstream 启停、Routing 切换和配置
|
非阻塞通道广播,连续变更自动合并。Admin 成功提交 Upstream 启停、Routing 切换和配置
|
||||||
发布时触发通知;Worker 保留原有定时刷新作为失效保护。Routing 切换不按旧 Upstream
|
发布时触发通知;Worker 保留原有定时刷新作为失效保护。Routing 切换不按旧 Upstream
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user