1860 lines
98 KiB
Go
1860 lines
98 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/capacity"
|
|
)
|
|
|
|
const defaultEndpoints = "gateway=http://127.0.0.1:20211,history=http://127.0.0.1:20212,stat=http://127.0.0.1:20213,bridge=http://127.0.0.1:20214,fields-projector=http://127.0.0.1:20218,realtime=http://127.0.0.1:20216,identity=http://127.0.0.1:20217,realtime-api=http://127.0.0.1:20200,fast-writer=http://127.0.0.1:20215"
|
|
const defaultRequiredServices = "gateway,history,stat,bridge,fields-projector,realtime,identity,realtime-api,fast-writer"
|
|
const defaultDailyMileageDiagnosticsURL = "http://127.0.0.1:20200/api/stats/daily-metrics/diagnostics/reasons"
|
|
const defaultDailyMileageDiagnosticsPath = "/api/stats/daily-metrics/diagnostics/reasons"
|
|
const defaultDailyMileageFieldStatusPath = "/api/stats/daily-metrics/diagnostics/field-status"
|
|
const defaultDailyMileageSourceQualityURL = "http://127.0.0.1:20200/api/stats/daily-metrics/sources/quality"
|
|
const defaultDailyMileageSourceQualityPath = "/api/stats/daily-metrics/sources/quality"
|
|
const defaultDailyMileageSourceSelectionURL = "http://127.0.0.1:20200/api/stats/daily-metrics/sources/selection"
|
|
const defaultDailyMileageSourceSelectionPath = "/api/stats/daily-metrics/sources/selection"
|
|
const defaultDailyMileageSourcesPath = "/api/stats/daily-metrics/sources"
|
|
const defaultDataSourceDiagnosticsURL = "http://127.0.0.1:20200/api/stats/data-sources/diagnostics"
|
|
const defaultDataSourceDiagnosticsPath = "/api/stats/data-sources/diagnostics"
|
|
const defaultDataSourceKindSuggestionsURL = "http://127.0.0.1:20200/api/stats/data-sources/kind-suggestions"
|
|
const defaultDataSourceKindSuggestionsPath = "/api/stats/data-sources/kind-suggestions"
|
|
const defaultDataSourceKindSuggestionProtocols = "GB32960,YUTONG_MQTT,JT808"
|
|
const defaultJT808IdentityGapsURL = "http://127.0.0.1:20200/api/stats/data-sources/jt808-identity-gaps"
|
|
const defaultJT808IdentityGapsPath = "/api/stats/data-sources/jt808-identity-gaps"
|
|
|
|
type endpoint struct {
|
|
Name string
|
|
URL string
|
|
}
|
|
|
|
func main() {
|
|
var rawEndpoints string
|
|
var rawRequiredServices string
|
|
var timeout time.Duration
|
|
thresholds := capacity.DefaultThresholds()
|
|
var requiredConsumerTopics string
|
|
var dailyMileageDiagnosticsURL string
|
|
var dailyMileageDiagnosticsDate string
|
|
var dailyMileageDiagnosticsMaxActionable int64
|
|
var dailyMileageDiagnosticsMaxPipeline int64
|
|
var dailyMileageDiagnosticsMaxSourceData int64
|
|
var dailyMileageDiagnosticsSampleLimit int
|
|
var dailyMileageSourceQualityURL string
|
|
var dailyMileageSourceQualityDate string
|
|
var dailyMileageSourceQualityLimit int
|
|
var dailyMileageSourceQualityMaxInvalidDelta int64
|
|
var dailyMileageSourceQualityMaxNoBaseline int64
|
|
var dailyMileageSourceQualityMaxFallbackAfterInvalidBaseline int64
|
|
var dailyMileageSourceQualityMaxRealtimeLocationFallback int64
|
|
var dailyMileageSourceSelectionURL string
|
|
var dailyMileageSourceSelectionDate string
|
|
var dailyMileageSourceSelectionLimit int
|
|
var dailyMileageSourceSelectionSampleLimit int
|
|
var dailyMileageSourceSelectionMinSelected int64
|
|
var dailyMileageSourceSelectionRequiredProtocols string
|
|
var dailyMileageSourceSelectionMinSelectedPerProtocol int64
|
|
var dailyMileageSourceSelectionMaxUnmanaged int64
|
|
var dailyMileageSourceSelectionMaxUnknownKind int64
|
|
var dailyMileageSourceSelectionMaxNotSelectedMileageKM float64
|
|
var dataSourceDiagnosticsURL string
|
|
var dataSourceDiagnosticsSampleLimit int
|
|
var dataSourceDiagnosticsReasonLimit int
|
|
var dataSourceDiagnosticsRecentAgeSeconds int64
|
|
var dataSourceDiagnosticsMaxMissingSourceCode int64
|
|
var dataSourceDiagnosticsMaxRecentMissingSourceCode int64
|
|
var dataSourceDiagnosticsMaxActionableCandidates int64
|
|
var dataSourceDiagnosticsMaxMappingIssues int64
|
|
var dataSourceDiagnosticsMaxRecentMappingIssues int64
|
|
var dataSourceKindSuggestionsURL string
|
|
var dataSourceKindSuggestionsProtocols string
|
|
var dataSourceKindSuggestionsSampleLimit int
|
|
var dataSourceKindSuggestionsReasonLimit int
|
|
var dataSourceKindSuggestionsRecentAgeSeconds int64
|
|
var dataSourceKindSuggestionsMaxPlatformCandidates int64
|
|
var dataSourceKindSuggestionsMaxRecentPlatformCandidates int64
|
|
var jt808IdentityGapsURL string
|
|
var jt808IdentityGapsRecentSeconds int64
|
|
var jt808IdentityGapsSampleLimit int
|
|
var jt808IdentityGapsMax int64
|
|
flag.StringVar(&rawEndpoints, "endpoints", defaultEndpoints, "comma separated name=url endpoints; /metrics is appended when no path is provided")
|
|
flag.StringVar(&rawRequiredServices, "required-services", defaultRequiredServices, "comma separated service names that must be present and successfully scraped; empty disables")
|
|
flag.DurationVar(&timeout, "timeout", 2*time.Second, "per endpoint timeout")
|
|
flag.Float64Var(&thresholds.GatewayFrameP99MS, "gateway-frame-p99-ms", thresholds.GatewayFrameP99MS, "degrade when gateway frame p99 exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.GatewayIdentityP99MS, "gateway-identity-p99-ms", thresholds.GatewayIdentityP99MS, "degrade when gateway identity resolve p99 exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.GatewayResponseRecentP99MS, "gateway-response-recent-p99-ms", thresholds.GatewayResponseRecentP99MS, "degrade when recent frame receipt to successful protocol response p99 exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.AsyncSinkP99MS, "async-sink-p99-ms", thresholds.AsyncSinkP99MS, "degrade when gateway async publish p99 exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.AsyncSinkQueueWaitRecentP99MS, "async-sink-queue-wait-recent-p99-ms", thresholds.AsyncSinkQueueWaitRecentP99MS, "degrade when recent gateway async queue wait p99 exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.BridgeBatchP99MS, "bridge-batch-p99-ms", thresholds.BridgeBatchP99MS, "degrade when NATS to Kafka bridge batch p99 exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.BridgeKafkaWriteP99MS, "bridge-kafka-write-p99-ms", thresholds.BridgeKafkaWriteP99MS, "degrade when NATS to Kafka bridge per-topic Kafka write p99 exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.BridgeKafkaE2EP99MS, "bridge-kafka-e2e-p99-ms", thresholds.BridgeKafkaE2EP99MS, "degrade when gateway received_at to Kafka bridge write p99 exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.BridgeKafkaRecentE2EP99MS, "bridge-kafka-recent-e2e-p99-ms", thresholds.BridgeKafkaRecentE2EP99MS, "degrade when recent gateway received_at to Kafka bridge write p99 exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.BridgeFetchWaitMaxMS, "bridge-fetch-wait-max-ms", thresholds.BridgeFetchWaitMaxMS, "degrade when bridge runtime fetch wait exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.BridgeWorkersMin, "bridge-workers-min", thresholds.BridgeWorkersMin, "degrade when bridge runtime worker count is below this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.BridgeRouteErrorMax, "bridge-route-error-max", thresholds.BridgeRouteErrorMax, "degrade when bridge unrouted NATS subject count exceeds this value; <0 disables")
|
|
flag.Float64Var(&thresholds.BridgeKafkaWriteErrorMax, "bridge-kafka-write-error-max", thresholds.BridgeKafkaWriteErrorMax, "degrade when bridge Kafka write errors exceed this value; <0 disables")
|
|
flag.Float64Var(&thresholds.BridgeMessageMinReceived, "bridge-message-min-received", thresholds.BridgeMessageMinReceived, "minimum bridge received messages per subject before unacked ratio is enforced; <=0 disables")
|
|
flag.Float64Var(&thresholds.BridgeMessageMaxUnackedRatio, "bridge-message-max-unacked-ratio", thresholds.BridgeMessageMaxUnackedRatio, "degrade when bridge unacked messages divided by received messages exceeds this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.FastWriterStageP99MS, "fast-writer-stage-p99-ms", thresholds.FastWriterStageP99MS, "degrade when NATS fast-writer stage p99 exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.FastWriterRedisE2EP99MS, "fast-writer-redis-e2e-p99-ms", thresholds.FastWriterRedisE2EP99MS, "degrade when gateway received_at to fast-writer Redis update p99 exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.FastWriterRedisRecentE2EP99MS, "fast-writer-redis-recent-e2e-p99-ms", thresholds.FastWriterRedisRecentE2EP99MS, "degrade when recent gateway received_at to fast-writer Redis update p99 exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.FastWriterFetchWaitMaxMS, "fast-writer-fetch-wait-max-ms", thresholds.FastWriterFetchWaitMaxMS, "degrade when fast-writer runtime fetch wait exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.FastWriterWorkersMin, "fast-writer-workers-min", thresholds.FastWriterWorkersMin, "degrade when fast-writer runtime worker count is below this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.FastWriterInvalidJSONMax, "fast-writer-invalid-json-max", thresholds.FastWriterInvalidJSONMax, "degrade when fast-writer invalid JSON count exceeds this value; <0 disables")
|
|
flag.Float64Var(&thresholds.FastWriterFallbackErrorMax, "fast-writer-fallback-error-max", thresholds.FastWriterFallbackErrorMax, "degrade when fast-writer batch fallback single writes still fail; <0 disables")
|
|
flag.Float64Var(&thresholds.FastWriterDecoupledUpdateErrorMax, "fast-writer-decoupled-update-error-max", thresholds.FastWriterDecoupledUpdateErrorMax, "degrade when fast-writer Redis catch-up after a decoupled storage failure also fails; <0 disables")
|
|
flag.Float64Var(&thresholds.FastWriterTDengineEnabledMax, "fast-writer-tdengine-enabled-max", thresholds.FastWriterTDengineEnabledMax, "degrade when fast-writer TDengine stage enabled gauge exceeds this value; <0 disables")
|
|
flag.Float64Var(&thresholds.HistoryFlushP99MS, "history-flush-p99-ms", thresholds.HistoryFlushP99MS, "degrade when history TDengine batch flush p99 exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.HistoryInvalidJSONMax, "history-invalid-json-max", thresholds.HistoryInvalidJSONMax, "degrade when history-writer invalid JSON count exceeds this value; <0 disables")
|
|
flag.Float64Var(&thresholds.HistoryWriteE2EP99MS, "history-write-e2e-p99-ms", thresholds.HistoryWriteE2EP99MS, "degrade when gateway received_at to history TDengine write p99 exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.HistoryWriteRecentE2EP99MS, "history-write-recent-e2e-p99-ms", thresholds.HistoryWriteRecentE2EP99MS, "degrade when recent gateway received_at to history TDengine write p99 exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.HistoryWorkersMin, "history-workers-min", thresholds.HistoryWorkersMin, "degrade when history-writer runtime worker count is below this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.HistoryLocationErrorMax, "history-location-error-max", thresholds.HistoryLocationErrorMax, "degrade when history-writer location derived write errors exceed this value; <0 disables")
|
|
flag.Float64Var(&thresholds.HistoryLocationAccountingMaxRatio, "history-location-accounting-max-mismatch-ratio", thresholds.HistoryLocationAccountingMaxRatio, "degrade when location derived status counters do not account for successful history raw writes by more than this ratio; <=0 disables")
|
|
flag.Float64Var(&thresholds.HistoryFallbackErrorMax, "history-fallback-error-max", thresholds.HistoryFallbackErrorMax, "degrade when history-writer batch fallback single writes still fail; <0 disables")
|
|
flag.Float64Var(&thresholds.HistoryRetryMax, "history-retry-max", thresholds.HistoryRetryMax, "degrade when history-writer transient write retries exceed this value; <0 disables")
|
|
flag.Float64Var(&thresholds.HistoryRetryExhaustedMax, "history-retry-exhausted-max", thresholds.HistoryRetryExhaustedMax, "degrade when history-writer exhausted transient write retries exceed this value; <0 disables")
|
|
flag.Float64Var(&thresholds.RealtimeBatchP99MS, "realtime-batch-p99-ms", thresholds.RealtimeBatchP99MS, "degrade when realtime projector whole-batch flush p99 exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.RealtimeStoreP99MS, "realtime-store-p99-ms", thresholds.RealtimeStoreP99MS, "degrade when realtime store update p99 exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.RealtimeWorkersMin, "realtime-workers-min", thresholds.RealtimeWorkersMin, "degrade when realtime-writer runtime worker count is below this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.RealtimeStoreErrorMax, "realtime-store-error-max", thresholds.RealtimeStoreErrorMax, "degrade when realtime Redis/MySQL store update errors exceed this value; <0 disables")
|
|
flag.Float64Var(&thresholds.RealtimeInvalidJSONMax, "realtime-invalid-json-max", thresholds.RealtimeInvalidJSONMax, "degrade when realtime projector invalid JSON count exceeds this value; <0 disables")
|
|
flag.Float64Var(&thresholds.StatWriteP99MS, "stat-write-p99-ms", thresholds.StatWriteP99MS, "degrade when stat write p99 exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.StatWriteE2EP99MS, "stat-write-e2e-p99-ms", thresholds.StatWriteE2EP99MS, "degrade when gateway received_at to stat MySQL write p99 exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.StatWriteRecentE2EP99MS, "stat-write-recent-e2e-p99-ms", thresholds.StatWriteRecentE2EP99MS, "degrade when recent gateway received_at to stat MySQL write p99 exceeds this many milliseconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.StatWorkersMin, "stat-workers-min", thresholds.StatWorkersMin, "degrade when stat-writer runtime worker count is below this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.IdentityWorkersMin, "identity-workers-min", thresholds.IdentityWorkersMin, "degrade when identity-writer runtime worker count is below this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.StatInvalidJSONMax, "stat-invalid-json-max", thresholds.StatInvalidJSONMax, "degrade when stat-writer invalid JSON count exceeds this value; <0 disables")
|
|
flag.Float64Var(&thresholds.StatProtocolTopicMismatchMax, "stat-protocol-topic-mismatch-max", thresholds.StatProtocolTopicMismatchMax, "degrade when stat-writer protocol/topic mismatch count exceeds this value; <0 disables")
|
|
flag.Float64Var(&thresholds.MessageContractMismatchMax, "message-contract-mismatch-max", thresholds.MessageContractMismatchMax, "degrade when consumer event kind or raw protocol/topic contract mismatch count exceeds this value; <0 disables")
|
|
flag.Float64Var(&thresholds.StatRetryMax, "stat-retry-max", thresholds.StatRetryMax, "degrade when stat-writer transient write retries exceed this value; <0 disables")
|
|
flag.Float64Var(&thresholds.StatRetryExhaustedMax, "stat-retry-exhausted-max", thresholds.StatRetryExhaustedMax, "degrade when stat-writer exhausted transient write retries exceed this value; <0 disables")
|
|
flag.Float64Var(&thresholds.ProcessUptimeMinSec, "process-uptime-min-seconds", thresholds.ProcessUptimeMinSec, "degrade when a service process has been up for fewer seconds than this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.ProcessHeapAllocMaxBytes, "process-heap-alloc-max-bytes", thresholds.ProcessHeapAllocMaxBytes, "degrade when a service heap allocation exceeds this many bytes; <=0 disables")
|
|
flag.Float64Var(&thresholds.ProcessHeapSysMaxBytes, "process-heap-sys-max-bytes", thresholds.ProcessHeapSysMaxBytes, "degrade when a service heap reservation exceeds this many bytes; <=0 disables")
|
|
flag.Float64Var(&thresholds.ProcessGoroutinesMax, "process-goroutines-max", thresholds.ProcessGoroutinesMax, "degrade when a service goroutine count exceeds this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.KafkaLagMax, "kafka-lag-max", thresholds.KafkaLagMax, "degrade when summed Kafka lag across history/stat/realtime exceeds this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.KafkaConsumerCommitErrorMax, "kafka-consumer-commit-error-max", thresholds.KafkaConsumerCommitErrorMax, "degrade when Kafka consumer commit errors per topic exceed this value; <0 disables")
|
|
flag.Float64Var(&thresholds.KafkaConsumerMessageMinReceived, "kafka-consumer-message-min-received", thresholds.KafkaConsumerMessageMinReceived, "minimum Kafka consumer received messages per topic before uncommitted ratio is enforced; <=0 disables")
|
|
flag.Float64Var(&thresholds.KafkaConsumerMaxUncommittedRatio, "kafka-consumer-max-uncommitted-ratio", thresholds.KafkaConsumerMaxUncommittedRatio, "degrade when Kafka consumer uncommitted messages divided by received messages exceeds this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.DurableSpoolBacklogMax, "durable-spool-backlog-max", thresholds.DurableSpoolBacklogMax, "degrade when durable spool backlog files exceed this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.DurableSpoolOldestAgeMaxSec, "durable-spool-oldest-age-max-seconds", thresholds.DurableSpoolOldestAgeMaxSec, "degrade when the oldest durable spool file is older than this many seconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.DurableSpoolErrorMax, "durable-spool-error-max", thresholds.DurableSpoolErrorMax, "degrade when durable spool record write errors exceed this value; <0 disables")
|
|
flag.Float64Var(&thresholds.DurableSpoolReplayErrorMax, "durable-spool-replay-error-max", thresholds.DurableSpoolReplayErrorMax, "degrade when durable spool replay errors exceed this value; <0 disables")
|
|
flag.Float64Var(&thresholds.DurableOutboxInflightMax, "durable-outbox-inflight-max", thresholds.DurableOutboxInflightMax, "degrade when durable outbox asynchronous publishes in flight exceed this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.DurableOutboxErrorMax, "durable-outbox-error-max", thresholds.DurableOutboxErrorMax, "degrade when durable outbox validation, submit, acknowledgement, removal, close, or quarantine errors exceed this value; <0 disables")
|
|
flag.Float64Var(&thresholds.AsyncSinkEnqueueErrorMax, "async-sink-enqueue-error-max", thresholds.AsyncSinkEnqueueErrorMax, "degrade when gateway async sink enqueue timeout or closed events exceed this value; <0 disables")
|
|
flag.Float64Var(&thresholds.AsyncSinkPublishErrorMax, "async-sink-publish-error-max", thresholds.AsyncSinkPublishErrorMax, "degrade when gateway async sink background publish errors exceed this value; <0 disables")
|
|
flag.Float64Var(&thresholds.AsyncSinkQueueDepthMax, "async-sink-queue-depth-max", thresholds.AsyncSinkQueueDepthMax, "degrade when gateway async sink queue depth exceeds this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.AsyncSinkQueueMaxUsageRatio, "async-sink-queue-max-usage-ratio", thresholds.AsyncSinkQueueMaxUsageRatio, "degrade when async sink queue depth divided by capacity exceeds this ratio; <=0 disables")
|
|
flag.Float64Var(&thresholds.FastWriterBatchPendingMax, "fast-writer-batch-pending-max", thresholds.FastWriterBatchPendingMax, "degrade when fast-writer in-flight batch messages exceed this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.HistoryBatchPendingMax, "history-batch-pending-max", thresholds.HistoryBatchPendingMax, "degrade when history in-flight batch messages exceed this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.HistoryRowsPendingMax, "history-rows-pending-max", thresholds.HistoryRowsPendingMax, "degrade when history in-flight TDengine rows exceed this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.RealtimeBatchPendingMax, "realtime-batch-pending-max", thresholds.RealtimeBatchPendingMax, "degrade when realtime in-flight batch messages exceed this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.RealtimeAsyncQueueDepthMax, "realtime-async-queue-depth-max", thresholds.RealtimeAsyncQueueDepthMax, "degrade when realtime async MySQL queue depth exceeds this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.RealtimeAsyncQueueMaxUsageRatio, "realtime-async-queue-max-usage-ratio", thresholds.RealtimeAsyncQueueMaxUsageRatio, "degrade when realtime async MySQL queue depth divided by capacity exceeds this ratio; <=0 disables")
|
|
flag.Float64Var(&thresholds.RealtimeAsyncQueueDroppedMax, "realtime-async-queue-dropped-max", thresholds.RealtimeAsyncQueueDroppedMax, "degrade when realtime async MySQL queue dropped or closed events exceed this value; <0 disables")
|
|
flag.Float64Var(&thresholds.HistoryParsedFieldMinFrames, "history-parsed-field-min-frames", thresholds.HistoryParsedFieldMinFrames, "minimum realtime history frames per topic/protocol before parsed field missing ratio is enforced; <=0 disables")
|
|
flag.Float64Var(&thresholds.HistoryParsedFieldMaxMissingRatio, "history-parsed-field-max-missing-ratio", thresholds.HistoryParsedFieldMaxMissingRatio, "degrade when history realtime frames missing parsed_fields divided by checked frames exceeds this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.MinHistogramSamples, "histogram-min-samples", thresholds.MinHistogramSamples, "minimum histogram sample count before latency thresholds are enforced")
|
|
flag.Float64Var(&thresholds.StatSampleMinWrites, "stat-sample-min-writes", thresholds.StatSampleMinWrites, "minimum successful stat append count per topic before requiring extracted mileage samples; <=0 disables")
|
|
flag.Float64Var(&thresholds.StatSampleMaxActionableSkipRatio, "stat-sample-max-actionable-skip-ratio", thresholds.StatSampleMaxActionableSkipRatio, "degrade when actionable stat sample skips divided by successful stat appends exceeds this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.StatSampleFutureAdjustmentMax, "stat-sample-future-adjustment-max", thresholds.StatSampleFutureAdjustmentMax, "degrade when stat-writer future event-time corrections exceed this value; <0 disables")
|
|
flag.Float64Var(&thresholds.StatSourceMinSamples, "stat-source-min-samples", thresholds.StatSourceMinSamples, "minimum stat samples found per topic before source tracking checks are enforced; <=0 disables")
|
|
flag.Float64Var(&thresholds.StatSourceMaxMissingRatio, "stat-source-max-missing-ratio", thresholds.StatSourceMaxMissingRatio, "degrade when source missing endpoint count divided by samples found exceeds this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.StatProjectionMinSampleWrites, "stat-projection-min-sample-writes", thresholds.StatProjectionMinSampleWrites, "minimum written stat samples per topic before requiring at least one final daily mileage projection; <=0 disables")
|
|
flag.Float64Var(&thresholds.StatBatchPendingMax, "stat-batch-pending-max", thresholds.StatBatchPendingMax, "degrade when stat-writer in-flight batch messages exceed this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.ConsumerRetryPendingMax, "consumer-retry-pending-max", thresholds.ConsumerRetryPendingMax, "degrade when history/realtime/stat/identity messages waiting for failure-closed retry exceed this value; <0 disables")
|
|
flag.Float64Var(&thresholds.StatCacheEntriesMax, "stat-cache-entries-max", thresholds.StatCacheEntriesMax, "degrade when any stat-writer in-memory cache entry gauge exceeds this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.RealtimeThrottleMinSamples, "realtime-throttle-min-samples", thresholds.RealtimeThrottleMinSamples, "minimum realtime throttle events per store/protocol before requiring some passed MySQL projection writes; <=0 disables")
|
|
flag.Float64Var(&thresholds.RealtimeThrottleCacheEntriesMax, "realtime-throttle-cache-entries-max", thresholds.RealtimeThrottleCacheEntriesMax, "degrade when realtime MySQL throttle cache entry gauge exceeds this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.RealtimePlateCacheEntriesMax, "realtime-plate-cache-entries-max", thresholds.RealtimePlateCacheEntriesMax, "degrade when realtime VIN-to-plate cache entry gauge exceeds this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.FastWriterRedisEnvelopeMinSeen, "fast-writer-redis-envelope-min-seen", thresholds.FastWriterRedisEnvelopeMinSeen, "minimum Redis realtime envelopes seen per raw subject before update/skip checks are enforced; <=0 disables")
|
|
flag.Float64Var(&thresholds.FastWriterRedisEnvelopeMaxBadRatio, "fast-writer-redis-envelope-max-bad-ratio", thresholds.FastWriterRedisEnvelopeMaxBadRatio, "degrade when missing-vin or missing-vehicle-key Redis envelopes divided by seen envelopes exceeds this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.FastWriterRedisFieldMinSeen, "fast-writer-redis-field-min-seen", thresholds.FastWriterRedisFieldMinSeen, "minimum Redis realtime fields seen per raw subject before stale field ratio is enforced; <=0 disables")
|
|
flag.Float64Var(&thresholds.FastWriterRedisFieldMaxStaleRatio, "fast-writer-redis-field-max-stale-ratio", thresholds.FastWriterRedisFieldMaxStaleRatio, "degrade when Redis stale/skipped realtime fields divided by seen fields exceeds this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.FastWriterMessageMinReceived, "fast-writer-message-min-received", thresholds.FastWriterMessageMinReceived, "minimum fast-writer received messages per raw subject before unacked ratio is enforced; <=0 disables")
|
|
flag.Float64Var(&thresholds.FastWriterMessageMaxUnackedRatio, "fast-writer-message-max-unacked-ratio", thresholds.FastWriterMessageMaxUnackedRatio, "degrade when fast-writer unacked messages divided by received messages exceeds this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.GatewayFieldMinRaw, "gateway-field-min-raw", thresholds.GatewayFieldMinRaw, "minimum gateway raw publishes per protocol before requiring at least one fields publish; <=0 disables")
|
|
flag.Float64Var(&thresholds.GatewayFieldMaxMissingRatio, "gateway-field-max-missing-ratio", thresholds.GatewayFieldMaxMissingRatio, "degrade when gateway fields missing/publish-error events divided by realtime candidate raw publishes exceeds this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.GatewayFieldMinCount, "gateway-field-min-count", thresholds.GatewayFieldMinCount, "degrade when a protocol has enough published fields events but the latest fields payload has fewer flattened fields than this; <=0 disables")
|
|
flag.Float64Var(&thresholds.GatewayBridgeMinPublish, "gateway-bridge-min-publishes", thresholds.GatewayBridgeMinPublish, "minimum gateway publishes per protocol/kind before requiring bridge Kafka writes to the expected topic; <=0 disables")
|
|
flag.Float64Var(&thresholds.GatewayFastWriterMinRawPublish, "gateway-fast-writer-min-raw-publishes", thresholds.GatewayFastWriterMinRawPublish, "minimum gateway raw publishes per protocol before requiring fast-writer messages on the expected raw subject; <=0 disables")
|
|
flag.Float64Var(&thresholds.GatewayParseMinFrames, "gateway-parse-min-frames", thresholds.GatewayParseMinFrames, "minimum gateway frames per protocol before non-OK parse ratio is enforced; <=0 disables")
|
|
flag.Float64Var(&thresholds.GatewayParseMaxBadRatio, "gateway-parse-max-bad-ratio", thresholds.GatewayParseMaxBadRatio, "degrade when gateway non-OK parse ratio exceeds this value after enough frames; <=0 disables")
|
|
flag.Float64Var(&thresholds.GatewayResponseMinSamples, "gateway-response-min-samples", thresholds.GatewayResponseMinSamples, "minimum gateway response attempts per protocol before response error ratio is enforced; <=0 disables")
|
|
flag.Float64Var(&thresholds.GatewayResponseMaxErrorRatio, "gateway-response-max-error-ratio", thresholds.GatewayResponseMaxErrorRatio, "degrade when gateway protocol response build/write error ratio exceeds this value after enough samples; <=0 disables")
|
|
flag.Float64Var(&thresholds.GatewayIdentityMinSamples, "gateway-identity-min-samples", thresholds.GatewayIdentityMinSamples, "minimum gateway identity samples per protocol before unresolved ratio is enforced; <=0 disables")
|
|
flag.Float64Var(&thresholds.GatewayIdentityMaxUnresolvedRatio, "gateway-identity-max-unresolved-ratio", thresholds.GatewayIdentityMaxUnresolvedRatio, "degrade when gateway non-resolved identity ratio exceeds this value after enough samples; <=0 disables")
|
|
flag.Float64Var(&thresholds.GatewayIdentityCacheEntriesMax, "gateway-identity-cache-entries-max", thresholds.GatewayIdentityCacheEntriesMax, "degrade when any gateway identity cache entry gauge exceeds this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.GatewayIdentitySnapshotRequired, "gateway-identity-snapshot-required", thresholds.GatewayIdentitySnapshotRequired, "degrade when the gateway identity snapshot is not ready; <=0 disables")
|
|
flag.Float64Var(&thresholds.GatewayIdentitySnapshotMaxAgeSec, "gateway-identity-snapshot-max-age-seconds", thresholds.GatewayIdentitySnapshotMaxAgeSec, "degrade when the last successful identity snapshot refresh is older than this many seconds; <=0 disables")
|
|
flag.Float64Var(&thresholds.GatewayIdentityLocationTouchFailureMax, "gateway-identity-location-touch-failure-max", thresholds.GatewayIdentityLocationTouchFailureMax, "degrade when jt808 registration location-touch failure backoff cache exceeds this value; <0 disables")
|
|
flag.Float64Var(&thresholds.GatewayIdentityRegistrationWriteQueueDepthMax, "gateway-identity-registration-write-queue-depth-max", thresholds.GatewayIdentityRegistrationWriteQueueDepthMax, "degrade when async JT808 registration write queue depth exceeds this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.GatewayIdentityRegistrationWriteQueueMaxUsageRatio, "gateway-identity-registration-write-queue-max-usage-ratio", thresholds.GatewayIdentityRegistrationWriteQueueMaxUsageRatio, "degrade when async JT808 registration write queue depth divided by capacity exceeds this ratio; <=0 disables")
|
|
flag.Float64Var(&thresholds.GatewayIdentityRegistrationWriteErrorMax, "gateway-identity-registration-write-error-max", thresholds.GatewayIdentityRegistrationWriteErrorMax, "degrade when async/sync JT808 registration write errors exceed this value; <0 disables")
|
|
flag.Float64Var(&thresholds.GatewayIdentityStaleMinSamples, "gateway-identity-stale-min-samples", thresholds.GatewayIdentityStaleMinSamples, "minimum gateway identity samples per protocol before stale cache ratio is enforced; <=0 disables")
|
|
flag.Float64Var(&thresholds.GatewayIdentityMaxStaleCacheRatio, "gateway-identity-max-stale-cache-ratio", thresholds.GatewayIdentityMaxStaleCacheRatio, "degrade when stale identity cache resolutions divided by identity samples exceeds this value; <=0 disables")
|
|
flag.Float64Var(&thresholds.RawFanoutMinBridge, "raw-fanout-min-bridge-writes", thresholds.RawFanoutMinBridge, "minimum bridge writes per raw topic before requiring history writes and realtime updates; <=0 disables")
|
|
flag.Float64Var(&thresholds.StatTopicMinBridge, "stat-topic-min-bridge-writes", thresholds.StatTopicMinBridge, "minimum bridge writes per fields topic before requiring stat consumer topic info and received messages; <=0 disables")
|
|
flag.Float64Var(&thresholds.LastActivityStaleSec, "last-activity-stale-seconds", thresholds.LastActivityStaleSec, "degrade when an observed successful protocol/topic activity timestamp is older than this many seconds; <=0 disables")
|
|
flag.StringVar(&requiredConsumerTopics, "required-consumer-topics", formatRequiredConsumerTopics(capacity.DefaultRequiredConsumerTopics()), "semicolon separated service=topic|topic contract; empty disables required topic checks")
|
|
flag.StringVar(&dailyMileageDiagnosticsURL, "daily-mileage-diagnostics-url", defaultDailyMileageDiagnosticsURL, "daily mileage diagnosis reason summary URL; empty disables this business-health probe")
|
|
flag.StringVar(&dailyMileageDiagnosticsDate, "daily-mileage-diagnostics-date", "", "YYYY-MM-DD stat date for daily mileage diagnostics; empty lets the API use local current date")
|
|
flag.Int64Var(&dailyMileageDiagnosticsMaxActionable, "daily-mileage-diagnostics-max-actionable", -1, "degrade when daily mileage actionable issues exceed this value; <0 reports only")
|
|
flag.Int64Var(&dailyMileageDiagnosticsMaxPipeline, "daily-mileage-diagnostics-max-pipeline", -1, "degrade when daily mileage pipeline issues exceed this value; <0 reports only")
|
|
flag.Int64Var(&dailyMileageDiagnosticsMaxSourceData, "daily-mileage-diagnostics-max-source-data", -1, "degrade when daily mileage source-data issues exceed this value; <0 reports only")
|
|
flag.IntVar(&dailyMileageDiagnosticsSampleLimit, "daily-mileage-diagnostics-sample-limit", 5, "maximum daily mileage diagnosis detail samples to attach to the report; <=0 disables samples")
|
|
flag.StringVar(&dailyMileageSourceQualityURL, "daily-mileage-source-quality-url", defaultDailyMileageSourceQualityURL, "daily mileage source quality summary URL; empty disables this business-health probe")
|
|
flag.StringVar(&dailyMileageSourceQualityDate, "daily-mileage-source-quality-date", "", "YYYY-MM-DD stat date for daily mileage source quality; empty uses Asia/Shanghai current date")
|
|
flag.IntVar(&dailyMileageSourceQualityLimit, "daily-mileage-source-quality-limit", 1000, "maximum daily mileage source quality summary rows to fetch; <=0 fetches 1 row")
|
|
flag.Int64Var(&dailyMileageSourceQualityMaxInvalidDelta, "daily-mileage-source-quality-max-invalid-delta", -1, "degrade when INVALID_DELTA source count exceeds this value; <0 reports only")
|
|
flag.Int64Var(&dailyMileageSourceQualityMaxNoBaseline, "daily-mileage-source-quality-max-no-baseline", -1, "degrade when NO_PREVIOUS_BASELINE source count exceeds this value; <0 reports only")
|
|
flag.Int64Var(&dailyMileageSourceQualityMaxFallbackAfterInvalidBaseline, "daily-mileage-source-quality-max-fallback-after-invalid-baseline", -1, "degrade when OK sources using current-day fallback after invalid baseline exceed this value; <0 reports only")
|
|
flag.Int64Var(&dailyMileageSourceQualityMaxRealtimeLocationFallback, "daily-mileage-source-quality-max-realtime-location-fallback", -1, "degrade when OK sources using realtime-location historical fallback exceed this value; <0 reports only")
|
|
flag.StringVar(&dailyMileageSourceSelectionURL, "daily-mileage-source-selection-url", defaultDailyMileageSourceSelectionURL, "daily mileage source selection summary URL; empty disables this business-health probe")
|
|
flag.StringVar(&dailyMileageSourceSelectionDate, "daily-mileage-source-selection-date", "", "YYYY-MM-DD stat date for daily mileage source selection; empty uses Asia/Shanghai current date")
|
|
flag.IntVar(&dailyMileageSourceSelectionLimit, "daily-mileage-source-selection-limit", 1000, "maximum daily mileage source selection summary rows to fetch; <=0 fetches 1 row")
|
|
flag.IntVar(&dailyMileageSourceSelectionSampleLimit, "daily-mileage-source-selection-sample-limit", 5, "maximum problematic daily mileage source selection detail samples to attach; <=0 disables samples")
|
|
flag.Int64Var(&dailyMileageSourceSelectionMinSelected, "daily-mileage-source-selection-min-selected", -1, "degrade when selected source count is below this value; <0 reports only")
|
|
flag.StringVar(&dailyMileageSourceSelectionRequiredProtocols, "daily-mileage-source-selection-required-protocols", defaultDataSourceKindSuggestionProtocols, "comma separated protocols that must each have selected mileage sources when per-protocol selection gating is enabled")
|
|
flag.Int64Var(&dailyMileageSourceSelectionMinSelectedPerProtocol, "daily-mileage-source-selection-min-selected-per-protocol", -1, "degrade when any required protocol has fewer selected source rows than this value; <0 reports only")
|
|
flag.Int64Var(&dailyMileageSourceSelectionMaxUnmanaged, "daily-mileage-source-selection-max-unmanaged", -1, "degrade when unmanaged selected-candidate source count exceeds this value; <0 reports only")
|
|
flag.Int64Var(&dailyMileageSourceSelectionMaxUnknownKind, "daily-mileage-source-selection-max-unknown-kind", -1, "degrade when UNKNOWN source_kind selected-candidate source count exceeds this value; <0 reports only")
|
|
flag.Float64Var(&dailyMileageSourceSelectionMaxNotSelectedMileageKM, "daily-mileage-source-selection-max-not-selected-mileage-km", -1, "degrade when not-selected source mileage sum exceeds this value; <0 reports only")
|
|
flag.StringVar(&dataSourceDiagnosticsURL, "data-source-diagnostics-url", defaultDataSourceDiagnosticsURL, "data source diagnostics URL for missing source_code; empty disables this business-health probe")
|
|
flag.IntVar(&dataSourceDiagnosticsSampleLimit, "data-source-diagnostics-sample-limit", 5, "maximum data source diagnosis samples to attach to the report; <=0 keeps total only")
|
|
flag.IntVar(&dataSourceDiagnosticsReasonLimit, "data-source-diagnostics-reason-limit", 1000, "maximum data source diagnosis rows to fetch for reason/actionable summary; <=0 disables reason summary")
|
|
flag.Int64Var(&dataSourceDiagnosticsRecentAgeSeconds, "data-source-diagnostics-recent-age-seconds", 300, "classify missing source_code data sources as recent when latest_seen_age_seconds is at or below this value; <=0 disables recent summary")
|
|
flag.Int64Var(&dataSourceDiagnosticsMaxMissingSourceCode, "data-source-diagnostics-max-missing-source-code", -1, "degrade when missing source_code data sources exceed this value; <0 reports only")
|
|
flag.Int64Var(&dataSourceDiagnosticsMaxRecentMissingSourceCode, "data-source-diagnostics-max-recent-missing-source-code", -1, "degrade when recent missing source_code data sources exceed this value; <0 reports only")
|
|
flag.Int64Var(&dataSourceDiagnosticsMaxActionableCandidates, "data-source-diagnostics-max-actionable-candidates", -1, "degrade when actionable data source candidates exceed this value; <0 reports only")
|
|
flag.Int64Var(&dataSourceDiagnosticsMaxMappingIssues, "data-source-diagnostics-max-mapping-issues", -1, "degrade when configured data source mapping issues exceed this value; <0 reports only")
|
|
flag.Int64Var(&dataSourceDiagnosticsMaxRecentMappingIssues, "data-source-diagnostics-max-recent-mapping-issues", -1, "degrade when recent configured data source mapping issues exceed this value; <0 reports only")
|
|
flag.StringVar(&dataSourceKindSuggestionsURL, "data-source-kind-suggestions-url", defaultDataSourceKindSuggestionsURL, "data source kind suggestion URL; empty disables this business-health probe")
|
|
flag.StringVar(&dataSourceKindSuggestionsProtocols, "data-source-kind-suggestions-protocols", defaultDataSourceKindSuggestionProtocols, "comma separated protocols to check for UNKNOWN source_kind suggestions")
|
|
flag.IntVar(&dataSourceKindSuggestionsSampleLimit, "data-source-kind-suggestions-sample-limit", 5, "maximum source_kind diagnosis samples to attach to the report; <=0 keeps total only")
|
|
flag.IntVar(&dataSourceKindSuggestionsReasonLimit, "data-source-kind-suggestions-reason-limit", 1000, "maximum source_kind diagnosis rows to fetch for suggestion summary; <=0 disables reason summary")
|
|
flag.Int64Var(&dataSourceKindSuggestionsRecentAgeSeconds, "data-source-kind-suggestions-recent-age-seconds", 300, "classify UNKNOWN source_kind rows as recent when latest_seen_age_seconds is at or below this value; <=0 disables recent summary")
|
|
flag.Int64Var(&dataSourceKindSuggestionsMaxPlatformCandidates, "data-source-kind-suggestions-max-platform-candidates", -1, "degrade when UNKNOWN source_kind rows that should be PLATFORM exceed this value; <0 reports only")
|
|
flag.Int64Var(&dataSourceKindSuggestionsMaxRecentPlatformCandidates, "data-source-kind-suggestions-max-recent-platform-candidates", 0, "degrade when recent UNKNOWN source_kind rows that should be PLATFORM exceed this value; <0 reports only")
|
|
flag.StringVar(&jt808IdentityGapsURL, "jt808-identity-gaps-url", defaultJT808IdentityGapsURL, "JT808 identity gap diagnostics URL; empty disables this business-health probe")
|
|
flag.Int64Var(&jt808IdentityGapsRecentSeconds, "jt808-identity-gaps-recent-seconds", 86400, "only count JT808 unresolved identity registrations seen within this many seconds; <=0 checks all rows")
|
|
flag.IntVar(&jt808IdentityGapsSampleLimit, "jt808-identity-gaps-sample-limit", 5, "maximum JT808 identity gap samples to attach to the report; <=0 keeps total only")
|
|
flag.Int64Var(&jt808IdentityGapsMax, "jt808-identity-gaps-max", -1, "degrade when JT808 unresolved identity gaps exceed this value; <0 reports only")
|
|
flag.Parse()
|
|
thresholds.RequiredConsumerTopics = parseRequiredConsumerTopics(requiredConsumerTopics)
|
|
|
|
requiredServices := parseRequiredServices(rawRequiredServices)
|
|
endpoints := parseEndpoints(rawEndpoints)
|
|
metricsCtx, metricsCancel := context.WithTimeout(context.Background(), timeout)
|
|
metricsByService, findings := collectMetrics(metricsCtx, endpoints, timeout)
|
|
metricsCancel()
|
|
findings = append(findings, requiredServiceFindings(endpoints, metricsByService, requiredServices)...)
|
|
report := capacity.EvaluateWithThresholds(metricsByService, thresholds)
|
|
report.Findings = append(report.Findings, findings...)
|
|
if strings.TrimSpace(dailyMileageDiagnosticsURL) != "" {
|
|
diagnosticsCtx, diagnosticsCancel := context.WithTimeout(context.Background(), timeout)
|
|
diagnostics, diagnosticsFindings := collectDailyMileageDiagnostics(diagnosticsCtx, dailyMileageDiagnosticsURL, dailyMileageDiagnosticsDate, timeout, dailyMileageDiagnosticsSampleLimit)
|
|
diagnosticsCancel()
|
|
if diagnostics != nil {
|
|
report.DailyMileageDiagnostics = diagnostics
|
|
report.Findings = append(report.Findings, dailyMileageDiagnosticFindings(
|
|
diagnostics,
|
|
dailyMileageDiagnosticsMaxActionable,
|
|
dailyMileageDiagnosticsMaxPipeline,
|
|
dailyMileageDiagnosticsMaxSourceData,
|
|
)...)
|
|
}
|
|
report.Findings = append(report.Findings, diagnosticsFindings...)
|
|
}
|
|
if strings.TrimSpace(dailyMileageSourceQualityURL) != "" {
|
|
sourceQualityCtx, sourceQualityCancel := context.WithTimeout(context.Background(), timeout)
|
|
diagnostics, diagnosticsFindings := collectDailyMileageSourceQuality(
|
|
sourceQualityCtx,
|
|
dailyMileageSourceQualityURL,
|
|
dailyMileageSourceQualityDate,
|
|
timeout,
|
|
dailyMileageSourceQualityLimit,
|
|
)
|
|
sourceQualityCancel()
|
|
if diagnostics != nil {
|
|
report.DailyMileageSourceQuality = diagnostics
|
|
report.Findings = append(report.Findings, dailyMileageSourceQualityFindings(
|
|
diagnostics,
|
|
dailyMileageSourceQualityMaxInvalidDelta,
|
|
dailyMileageSourceQualityMaxNoBaseline,
|
|
dailyMileageSourceQualityMaxFallbackAfterInvalidBaseline,
|
|
dailyMileageSourceQualityMaxRealtimeLocationFallback,
|
|
)...)
|
|
}
|
|
report.Findings = append(report.Findings, diagnosticsFindings...)
|
|
}
|
|
if strings.TrimSpace(dailyMileageSourceSelectionURL) != "" {
|
|
sourceSelectionCtx, sourceSelectionCancel := context.WithTimeout(context.Background(), timeout)
|
|
diagnostics, diagnosticsFindings := collectDailyMileageSourceSelection(
|
|
sourceSelectionCtx,
|
|
dailyMileageSourceSelectionURL,
|
|
dailyMileageSourceSelectionDate,
|
|
timeout,
|
|
dailyMileageSourceSelectionLimit,
|
|
dailyMileageSourceSelectionSampleLimit,
|
|
)
|
|
sourceSelectionCancel()
|
|
if diagnostics != nil {
|
|
report.DailyMileageSourceSelection = diagnostics
|
|
report.Findings = append(report.Findings, dailyMileageSourceSelectionFindings(
|
|
diagnostics,
|
|
dailyMileageSourceSelectionMinSelected,
|
|
parseDataSourceKindSuggestionProtocols(dailyMileageSourceSelectionRequiredProtocols),
|
|
dailyMileageSourceSelectionMinSelectedPerProtocol,
|
|
dailyMileageSourceSelectionMaxUnmanaged,
|
|
dailyMileageSourceSelectionMaxUnknownKind,
|
|
dailyMileageSourceSelectionMaxNotSelectedMileageKM,
|
|
)...)
|
|
}
|
|
report.Findings = append(report.Findings, diagnosticsFindings...)
|
|
}
|
|
if strings.TrimSpace(dataSourceDiagnosticsURL) != "" {
|
|
dataSourceCtx, dataSourceCancel := context.WithTimeout(context.Background(), timeout)
|
|
diagnostics, diagnosticsFindings := collectDataSourceDiagnostics(
|
|
dataSourceCtx,
|
|
dataSourceDiagnosticsURL,
|
|
timeout,
|
|
dataSourceDiagnosticsSampleLimit,
|
|
dataSourceDiagnosticsReasonLimit,
|
|
dataSourceDiagnosticsRecentAgeSeconds,
|
|
)
|
|
dataSourceCancel()
|
|
if diagnostics != nil {
|
|
report.DataSourceDiagnostics = diagnostics
|
|
report.Findings = append(report.Findings, dataSourceDiagnosticFindings(
|
|
diagnostics,
|
|
dataSourceDiagnosticsMaxMissingSourceCode,
|
|
dataSourceDiagnosticsMaxRecentMissingSourceCode,
|
|
dataSourceDiagnosticsMaxActionableCandidates,
|
|
dataSourceDiagnosticsMaxMappingIssues,
|
|
dataSourceDiagnosticsMaxRecentMappingIssues,
|
|
)...)
|
|
}
|
|
report.Findings = append(report.Findings, diagnosticsFindings...)
|
|
}
|
|
if strings.TrimSpace(dataSourceKindSuggestionsURL) != "" {
|
|
dataSourceKindCtx, dataSourceKindCancel := context.WithTimeout(context.Background(), timeout)
|
|
diagnostics, diagnosticsFindings := collectDataSourceKindSuggestions(
|
|
dataSourceKindCtx,
|
|
dataSourceKindSuggestionsURL,
|
|
timeout,
|
|
parseDataSourceKindSuggestionProtocols(dataSourceKindSuggestionsProtocols),
|
|
dataSourceKindSuggestionsSampleLimit,
|
|
dataSourceKindSuggestionsReasonLimit,
|
|
dataSourceKindSuggestionsRecentAgeSeconds,
|
|
)
|
|
dataSourceKindCancel()
|
|
if diagnostics != nil {
|
|
report.DataSourceKindDiagnostics = diagnostics
|
|
report.Findings = append(report.Findings, dataSourceKindDiagnosticFindings(
|
|
diagnostics,
|
|
dataSourceKindSuggestionsMaxPlatformCandidates,
|
|
dataSourceKindSuggestionsMaxRecentPlatformCandidates,
|
|
)...)
|
|
}
|
|
report.Findings = append(report.Findings, diagnosticsFindings...)
|
|
}
|
|
if strings.TrimSpace(jt808IdentityGapsURL) != "" {
|
|
identityGapCtx, identityGapCancel := context.WithTimeout(context.Background(), timeout)
|
|
diagnostics, diagnosticsFindings := collectJT808IdentityGaps(
|
|
identityGapCtx,
|
|
jt808IdentityGapsURL,
|
|
timeout,
|
|
jt808IdentityGapsRecentSeconds,
|
|
jt808IdentityGapsSampleLimit,
|
|
)
|
|
identityGapCancel()
|
|
if diagnostics != nil {
|
|
report.JT808IdentityGaps = diagnostics
|
|
report.Findings = append(report.Findings, jt808IdentityGapFindings(diagnostics, jt808IdentityGapsMax)...)
|
|
}
|
|
report.Findings = append(report.Findings, diagnosticsFindings...)
|
|
}
|
|
if len(report.Findings) > 0 {
|
|
report.Status = capacity.StatusDegraded
|
|
}
|
|
encoder := json.NewEncoder(os.Stdout)
|
|
encoder.SetIndent("", " ")
|
|
_ = encoder.Encode(report)
|
|
if report.Status != capacity.StatusOK {
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
func dailyMileageDiagnosticFindings(diagnostics *capacity.DailyMileageDiagnostics, maxActionable, maxPipeline, maxSourceData int64) []string {
|
|
if diagnostics == nil {
|
|
return nil
|
|
}
|
|
var findings []string
|
|
if maxActionable >= 0 && diagnostics.ActionableIssueTotal > maxActionable {
|
|
findings = append(findings, fmt.Sprintf(
|
|
"daily mileage actionable issues %d exceeds %d (pipeline=%d, source_data=%d)",
|
|
diagnostics.ActionableIssueTotal,
|
|
maxActionable,
|
|
diagnostics.PipelineIssueTotal,
|
|
diagnostics.SourceDataIssueTotal,
|
|
))
|
|
}
|
|
if maxPipeline >= 0 && diagnostics.PipelineIssueTotal > maxPipeline {
|
|
findings = append(findings, fmt.Sprintf(
|
|
"daily mileage pipeline issues %d exceeds %d (source_data=%d)",
|
|
diagnostics.PipelineIssueTotal,
|
|
maxPipeline,
|
|
diagnostics.SourceDataIssueTotal,
|
|
))
|
|
}
|
|
if maxSourceData >= 0 && diagnostics.SourceDataIssueTotal > maxSourceData {
|
|
findings = append(findings, fmt.Sprintf(
|
|
"daily mileage source-data issues %d exceeds %d (pipeline=%d)",
|
|
diagnostics.SourceDataIssueTotal,
|
|
maxSourceData,
|
|
diagnostics.PipelineIssueTotal,
|
|
))
|
|
}
|
|
return findings
|
|
}
|
|
|
|
func dailyMileageSourceQualityFindings(diagnostics *capacity.DailyMileageSourceQualityDiagnostics, maxInvalidDelta, maxNoBaseline, maxFallbackAfterInvalidBaseline, maxRealtimeLocationFallback int64) []string {
|
|
if diagnostics == nil {
|
|
return nil
|
|
}
|
|
var findings []string
|
|
if maxInvalidDelta >= 0 && diagnostics.InvalidDeltaSourceCount > maxInvalidDelta {
|
|
findings = append(findings, fmt.Sprintf(
|
|
"daily mileage source INVALID_DELTA count %d exceeds %d",
|
|
diagnostics.InvalidDeltaSourceCount,
|
|
maxInvalidDelta,
|
|
))
|
|
}
|
|
if maxNoBaseline >= 0 && diagnostics.NoBaselineSourceCount > maxNoBaseline {
|
|
findings = append(findings, fmt.Sprintf(
|
|
"daily mileage source NO_PREVIOUS_BASELINE count %d exceeds %d",
|
|
diagnostics.NoBaselineSourceCount,
|
|
maxNoBaseline,
|
|
))
|
|
}
|
|
if maxFallbackAfterInvalidBaseline >= 0 && diagnostics.FallbackAfterInvalidBaselineSourceCount > maxFallbackAfterInvalidBaseline {
|
|
findings = append(findings, fmt.Sprintf(
|
|
"daily mileage source current-day fallback after invalid baseline count %d exceeds %d",
|
|
diagnostics.FallbackAfterInvalidBaselineSourceCount,
|
|
maxFallbackAfterInvalidBaseline,
|
|
))
|
|
}
|
|
if maxRealtimeLocationFallback >= 0 && diagnostics.RealtimeLocationFallbackSourceCount > maxRealtimeLocationFallback {
|
|
findings = append(findings, fmt.Sprintf(
|
|
"daily mileage source realtime-location fallback count %d exceeds %d",
|
|
diagnostics.RealtimeLocationFallbackSourceCount,
|
|
maxRealtimeLocationFallback,
|
|
))
|
|
}
|
|
return findings
|
|
}
|
|
|
|
func dailyMileageSourceSelectionFindings(diagnostics *capacity.DailyMileageSourceSelectionDiagnostics, minSelected int64, requiredProtocols []string, minSelectedPerProtocol int64, maxUnmanaged, maxUnknownKind int64, maxNotSelectedMileageKM float64) []string {
|
|
if diagnostics == nil {
|
|
return nil
|
|
}
|
|
var findings []string
|
|
if minSelected >= 0 && diagnostics.SelectedSourceCount < minSelected {
|
|
findings = append(findings, fmt.Sprintf(
|
|
"daily mileage selected source count %d is below %d",
|
|
diagnostics.SelectedSourceCount,
|
|
minSelected,
|
|
))
|
|
}
|
|
if minSelectedPerProtocol >= 0 {
|
|
byProtocol := dailyMileageSourceSelectionProtocolsByName(diagnostics.Protocols)
|
|
for _, protocol := range requiredProtocols {
|
|
protocol = strings.ToUpper(strings.TrimSpace(protocol))
|
|
if protocol == "" {
|
|
continue
|
|
}
|
|
selected := byProtocol[protocol].SelectedSourceCount
|
|
if selected >= minSelectedPerProtocol {
|
|
continue
|
|
}
|
|
findings = append(findings, fmt.Sprintf(
|
|
"daily mileage selected source count for protocol %s is %d below %d",
|
|
protocol,
|
|
selected,
|
|
minSelectedPerProtocol,
|
|
))
|
|
}
|
|
}
|
|
if maxUnmanaged >= 0 && diagnostics.UnmanagedSourceCount > maxUnmanaged {
|
|
findings = append(findings, fmt.Sprintf(
|
|
"daily mileage unmanaged source count %d exceeds %d",
|
|
diagnostics.UnmanagedSourceCount,
|
|
maxUnmanaged,
|
|
))
|
|
}
|
|
if maxUnknownKind >= 0 && diagnostics.UnknownKindSourceCount > maxUnknownKind {
|
|
findings = append(findings, fmt.Sprintf(
|
|
"daily mileage UNKNOWN source_kind count %d exceeds %d",
|
|
diagnostics.UnknownKindSourceCount,
|
|
maxUnknownKind,
|
|
))
|
|
}
|
|
if maxNotSelectedMileageKM >= 0 && diagnostics.NotSelectedMileageKM > maxNotSelectedMileageKM {
|
|
findings = append(findings, fmt.Sprintf(
|
|
"daily mileage not-selected source mileage %.1fkm exceeds %.1fkm",
|
|
diagnostics.NotSelectedMileageKM,
|
|
maxNotSelectedMileageKM,
|
|
))
|
|
}
|
|
return findings
|
|
}
|
|
|
|
func dailyMileageSourceSelectionProtocolsByName(items []capacity.DailyMileageSourceSelectionProtocol) map[string]capacity.DailyMileageSourceSelectionProtocol {
|
|
out := make(map[string]capacity.DailyMileageSourceSelectionProtocol, len(items))
|
|
for _, item := range items {
|
|
protocol := strings.ToUpper(strings.TrimSpace(item.Protocol))
|
|
if protocol == "" {
|
|
continue
|
|
}
|
|
out[protocol] = item
|
|
}
|
|
return out
|
|
}
|
|
|
|
func dataSourceDiagnosticFindings(diagnostics *capacity.DataSourceDiagnostics, maxMissingSourceCode, maxRecentMissingSourceCode, maxActionableCandidates, maxMappingIssues, maxRecentMappingIssues int64) []string {
|
|
if diagnostics == nil {
|
|
return nil
|
|
}
|
|
var findings []string
|
|
if maxMissingSourceCode >= 0 && diagnostics.MissingSourceCodeTotal > maxMissingSourceCode {
|
|
findings = append(findings, fmt.Sprintf(
|
|
"data source missing source_code %d exceeds %d",
|
|
diagnostics.MissingSourceCodeTotal,
|
|
maxMissingSourceCode,
|
|
))
|
|
}
|
|
if maxRecentMissingSourceCode >= 0 && diagnostics.RecentMissingSourceCodeTotal > maxRecentMissingSourceCode {
|
|
findings = append(findings, fmt.Sprintf(
|
|
"data source recent missing source_code %d exceeds %d (recent_age_seconds=%d)",
|
|
diagnostics.RecentMissingSourceCodeTotal,
|
|
maxRecentMissingSourceCode,
|
|
diagnostics.RecentAgeThresholdSeconds,
|
|
))
|
|
}
|
|
if maxActionableCandidates >= 0 && diagnostics.ActionableCandidateTotal > maxActionableCandidates {
|
|
findings = append(findings, fmt.Sprintf(
|
|
"data source actionable candidates %d exceeds %d",
|
|
diagnostics.ActionableCandidateTotal,
|
|
maxActionableCandidates,
|
|
))
|
|
}
|
|
if maxMappingIssues >= 0 && diagnostics.MappingIssueTotal > maxMappingIssues {
|
|
findings = append(findings, fmt.Sprintf(
|
|
"data source mapping issues %d exceeds %d",
|
|
diagnostics.MappingIssueTotal,
|
|
maxMappingIssues,
|
|
))
|
|
}
|
|
if maxRecentMappingIssues >= 0 && diagnostics.RecentMappingIssueTotal > maxRecentMappingIssues {
|
|
findings = append(findings, fmt.Sprintf(
|
|
"data source recent mapping issues %d exceeds %d (recent_age_seconds=%d)",
|
|
diagnostics.RecentMappingIssueTotal,
|
|
maxRecentMappingIssues,
|
|
diagnostics.RecentAgeThresholdSeconds,
|
|
))
|
|
}
|
|
return findings
|
|
}
|
|
|
|
func dataSourceKindDiagnosticFindings(diagnostics *capacity.DataSourceDiagnostics, maxPlatformCandidates, maxRecentPlatformCandidates int64) []string {
|
|
if diagnostics == nil {
|
|
return nil
|
|
}
|
|
var findings []string
|
|
if maxPlatformCandidates >= 0 && diagnostics.PlatformKindCandidateTotal > maxPlatformCandidates {
|
|
findings = append(findings, fmt.Sprintf(
|
|
"data source UNKNOWN source_kind platform candidates %d exceeds %d",
|
|
diagnostics.PlatformKindCandidateTotal,
|
|
maxPlatformCandidates,
|
|
))
|
|
}
|
|
if maxRecentPlatformCandidates >= 0 && diagnostics.RecentPlatformKindCandidateTotal > maxRecentPlatformCandidates {
|
|
findings = append(findings, fmt.Sprintf(
|
|
"data source recent UNKNOWN source_kind platform candidates %d exceeds %d (recent_age_seconds=%d)",
|
|
diagnostics.RecentPlatformKindCandidateTotal,
|
|
maxRecentPlatformCandidates,
|
|
diagnostics.RecentAgeThresholdSeconds,
|
|
))
|
|
}
|
|
return findings
|
|
}
|
|
|
|
func jt808IdentityGapFindings(diagnostics *capacity.JT808IdentityGapDiagnostics, maxGaps int64) []string {
|
|
if diagnostics == nil {
|
|
return nil
|
|
}
|
|
if maxGaps < 0 || diagnostics.Total <= maxGaps {
|
|
return nil
|
|
}
|
|
return []string{fmt.Sprintf(
|
|
"JT808 unresolved identity gaps %d exceeds %d (recent_seconds=%d)",
|
|
diagnostics.Total,
|
|
maxGaps,
|
|
diagnostics.RecentSeconds,
|
|
)}
|
|
}
|
|
|
|
func formatRequiredConsumerTopics(values map[string][]string) string {
|
|
if len(values) == 0 {
|
|
return ""
|
|
}
|
|
var services []string
|
|
for service := range values {
|
|
service = strings.TrimSpace(service)
|
|
if service == "" {
|
|
continue
|
|
}
|
|
services = append(services, service)
|
|
}
|
|
sort.Strings(services)
|
|
parts := make([]string, 0, len(services))
|
|
for _, service := range services {
|
|
var topics []string
|
|
for _, topic := range values[service] {
|
|
topic = strings.TrimSpace(topic)
|
|
if topic == "" {
|
|
continue
|
|
}
|
|
topics = append(topics, topic)
|
|
}
|
|
if len(topics) == 0 {
|
|
continue
|
|
}
|
|
sort.Strings(topics)
|
|
parts = append(parts, service+"="+strings.Join(topics, "|"))
|
|
}
|
|
return strings.Join(parts, ";")
|
|
}
|
|
|
|
func parseRequiredConsumerTopics(value string) map[string][]string {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
out := map[string][]string{}
|
|
for _, serviceSpec := range strings.Split(value, ";") {
|
|
serviceSpec = strings.TrimSpace(serviceSpec)
|
|
if serviceSpec == "" {
|
|
continue
|
|
}
|
|
service, rawTopics, ok := strings.Cut(serviceSpec, "=")
|
|
if !ok {
|
|
continue
|
|
}
|
|
service = strings.TrimSpace(service)
|
|
if service == "" {
|
|
continue
|
|
}
|
|
seen := map[string]struct{}{}
|
|
for _, topic := range strings.Split(rawTopics, "|") {
|
|
topic = strings.TrimSpace(topic)
|
|
if topic == "" {
|
|
continue
|
|
}
|
|
if _, exists := seen[topic]; exists {
|
|
continue
|
|
}
|
|
seen[topic] = struct{}{}
|
|
out[service] = append(out[service], topic)
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
func parseRequiredServices(value string) []string {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
seen := map[string]struct{}{}
|
|
var services []string
|
|
for _, item := range strings.Split(value, ",") {
|
|
item = strings.TrimSpace(item)
|
|
if item == "" {
|
|
continue
|
|
}
|
|
if _, exists := seen[item]; exists {
|
|
continue
|
|
}
|
|
seen[item] = struct{}{}
|
|
services = append(services, item)
|
|
}
|
|
return services
|
|
}
|
|
|
|
func parseDataSourceKindSuggestionProtocols(value string) []string {
|
|
protocols := parseRequiredServices(value)
|
|
if len(protocols) == 0 {
|
|
protocols = parseRequiredServices(defaultDataSourceKindSuggestionProtocols)
|
|
}
|
|
for i, protocol := range protocols {
|
|
protocols[i] = strings.ToUpper(strings.TrimSpace(protocol))
|
|
}
|
|
return protocols
|
|
}
|
|
|
|
func requiredServiceFindings(endpoints []endpoint, metricsByService map[string]string, requiredServices []string) []string {
|
|
if len(requiredServices) == 0 {
|
|
return nil
|
|
}
|
|
configured := map[string]struct{}{}
|
|
for _, ep := range endpoints {
|
|
if ep.Name == "" {
|
|
continue
|
|
}
|
|
configured[ep.Name] = struct{}{}
|
|
}
|
|
var findings []string
|
|
for _, service := range requiredServices {
|
|
if _, ok := configured[service]; !ok {
|
|
findings = append(findings, fmt.Sprintf("required service endpoint missing: %s", service))
|
|
continue
|
|
}
|
|
if _, ok := metricsByService[service]; !ok {
|
|
findings = append(findings, fmt.Sprintf("required service metrics missing: %s", service))
|
|
}
|
|
}
|
|
return findings
|
|
}
|
|
|
|
func parseEndpoints(value string) []endpoint {
|
|
var endpoints []endpoint
|
|
for _, item := range strings.Split(value, ",") {
|
|
item = strings.TrimSpace(item)
|
|
if item == "" {
|
|
continue
|
|
}
|
|
name, url, ok := strings.Cut(item, "=")
|
|
if !ok {
|
|
continue
|
|
}
|
|
name = strings.TrimSpace(name)
|
|
url = withMetricsPath(strings.TrimSpace(url))
|
|
if name == "" || url == "" {
|
|
continue
|
|
}
|
|
endpoints = append(endpoints, endpoint{Name: name, URL: url})
|
|
}
|
|
return endpoints
|
|
}
|
|
|
|
func withMetricsPath(value string) string {
|
|
parsed, err := url.Parse(value)
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
return value
|
|
}
|
|
if parsed.Path == "" || parsed.Path == "/" {
|
|
parsed.Path = "/metrics"
|
|
}
|
|
return parsed.String()
|
|
}
|
|
|
|
func collectMetrics(ctx context.Context, endpoints []endpoint, timeout time.Duration) (map[string]string, []string) {
|
|
client := &http.Client{Timeout: timeout}
|
|
metricsByService := make(map[string]string, len(endpoints))
|
|
var findings []string
|
|
for _, ep := range endpoints {
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, ep.URL, nil)
|
|
if err != nil {
|
|
findings = append(findings, fmt.Sprintf("%s metrics request invalid: %v", ep.Name, err))
|
|
continue
|
|
}
|
|
response, err := client.Do(request)
|
|
if err != nil {
|
|
findings = append(findings, fmt.Sprintf("%s metrics fetch failed: %v", ep.Name, err))
|
|
continue
|
|
}
|
|
body, readErr := io.ReadAll(response.Body)
|
|
_ = response.Body.Close()
|
|
if readErr != nil {
|
|
findings = append(findings, fmt.Sprintf("%s metrics read failed: %v", ep.Name, readErr))
|
|
continue
|
|
}
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
findings = append(findings, fmt.Sprintf("%s metrics status %d", ep.Name, response.StatusCode))
|
|
continue
|
|
}
|
|
metricsByService[ep.Name] = string(body)
|
|
}
|
|
return metricsByService, findings
|
|
}
|
|
|
|
func collectDailyMileageDiagnostics(ctx context.Context, rawURL string, date string, timeout time.Duration, sampleLimit int) (*capacity.DailyMileageDiagnostics, []string) {
|
|
requestURL, err := dailyMileageDiagnosticsRequestURL(rawURL, date)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage diagnostics request invalid: %v", err)}
|
|
}
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage diagnostics request invalid: %v", err)}
|
|
}
|
|
response, err := (&http.Client{Timeout: timeout}).Do(request)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage diagnostics fetch failed: %v", err)}
|
|
}
|
|
defer response.Body.Close()
|
|
body, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage diagnostics read failed: %v", err)}
|
|
}
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return nil, []string{fmt.Sprintf("daily mileage diagnostics status %d: %s", response.StatusCode, strings.TrimSpace(string(body)))}
|
|
}
|
|
var payload struct {
|
|
Items []capacity.DailyMileageDiagnosisReasonItem `json:"items"`
|
|
VehicleTotal int64 `json:"vehicle_total"`
|
|
ActionableIssueTotal int64 `json:"actionable_issue_total"`
|
|
}
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage diagnostics decode failed: %v", err)}
|
|
}
|
|
items, pipelineIssues, sourceDataIssues := capacity.AnnotateDailyMileageDiagnostics(payload.Items)
|
|
samples, sampleFindings := collectDailyMileageDiagnosisSamples(ctx, rawURL, date, timeout, items, sampleLimit)
|
|
fieldStatus, fieldStatusFindings := collectDailyMileageFieldStatus(ctx, rawURL, date, timeout)
|
|
findings := append(sampleFindings, fieldStatusFindings...)
|
|
return &capacity.DailyMileageDiagnostics{
|
|
URL: requestURL,
|
|
VehicleTotal: payload.VehicleTotal,
|
|
ActionableIssueTotal: payload.ActionableIssueTotal,
|
|
PipelineIssueTotal: pipelineIssues,
|
|
SourceDataIssueTotal: sourceDataIssues,
|
|
Items: items,
|
|
Samples: samples,
|
|
FieldStatus: fieldStatus,
|
|
}, findings
|
|
}
|
|
|
|
func collectDailyMileageFieldStatus(ctx context.Context, rawURL string, date string, timeout time.Duration) (*capacity.DailyMileageFieldStatusDiagnostics, []string) {
|
|
requestURL, err := dailyMileageFieldStatusRequestURL(rawURL, date)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage field status request invalid: %v", err)}
|
|
}
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage field status request invalid: %v", err)}
|
|
}
|
|
response, err := (&http.Client{Timeout: timeout}).Do(request)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage field status fetch failed: %v", err)}
|
|
}
|
|
defer response.Body.Close()
|
|
body, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage field status read failed: %v", err)}
|
|
}
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return nil, []string{fmt.Sprintf("daily mileage field status status %d: %s", response.StatusCode, strings.TrimSpace(string(body)))}
|
|
}
|
|
var payload struct {
|
|
Items []capacity.DailyMileageFieldStatusSummaryItem `json:"items"`
|
|
Total int64 `json:"total"`
|
|
VehicleTotal int64 `json:"vehicle_total"`
|
|
ActionableIssueTotal int64 `json:"actionable_issue_total"`
|
|
PipelineIssueTotal int64 `json:"pipeline_issue_total"`
|
|
SourceDataIssueTotal int64 `json:"source_data_issue_total"`
|
|
}
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage field status decode failed: %v", err)}
|
|
}
|
|
actionable, pipeline, sourceData := summarizeDailyMileageFieldStatus(payload.Items)
|
|
if payload.ActionableIssueTotal == 0 && actionable > 0 {
|
|
payload.ActionableIssueTotal = actionable
|
|
}
|
|
if payload.PipelineIssueTotal == 0 && pipeline > 0 {
|
|
payload.PipelineIssueTotal = pipeline
|
|
}
|
|
if payload.SourceDataIssueTotal == 0 && sourceData > 0 {
|
|
payload.SourceDataIssueTotal = sourceData
|
|
}
|
|
if payload.VehicleTotal == 0 {
|
|
for _, item := range payload.Items {
|
|
payload.VehicleTotal += item.Count
|
|
}
|
|
}
|
|
return &capacity.DailyMileageFieldStatusDiagnostics{
|
|
URL: requestURL,
|
|
SummaryTotal: payload.Total,
|
|
VehicleTotal: payload.VehicleTotal,
|
|
ActionableIssueTotal: payload.ActionableIssueTotal,
|
|
PipelineIssueTotal: payload.PipelineIssueTotal,
|
|
SourceDataIssueTotal: payload.SourceDataIssueTotal,
|
|
Items: payload.Items,
|
|
}, nil
|
|
}
|
|
|
|
func summarizeDailyMileageFieldStatus(items []capacity.DailyMileageFieldStatusSummaryItem) (actionable, pipeline, sourceData int64) {
|
|
for _, item := range items {
|
|
severity := strings.ToLower(strings.TrimSpace(item.Severity))
|
|
if severity == "" {
|
|
severity = strings.ToLower(strings.TrimSpace(item.FieldStatusSeverity))
|
|
}
|
|
switch severity {
|
|
case capacity.DailyMileageSeverityPipeline:
|
|
actionable += item.Count
|
|
pipeline += item.Count
|
|
case capacity.DailyMileageSeveritySourceData:
|
|
actionable += item.Count
|
|
sourceData += item.Count
|
|
default:
|
|
if !strings.EqualFold(strings.TrimSpace(item.Diagnosis), "OK") {
|
|
actionable += item.Count
|
|
}
|
|
}
|
|
}
|
|
return actionable, pipeline, sourceData
|
|
}
|
|
|
|
func collectDailyMileageSourceQuality(ctx context.Context, rawURL string, date string, timeout time.Duration, limit int) (*capacity.DailyMileageSourceQualityDiagnostics, []string) {
|
|
requestURL, err := dailyMileageSourceQualityRequestURL(rawURL, date, limit)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage source quality request invalid: %v", err)}
|
|
}
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage source quality request invalid: %v", err)}
|
|
}
|
|
response, err := (&http.Client{Timeout: timeout}).Do(request)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage source quality fetch failed: %v", err)}
|
|
}
|
|
defer response.Body.Close()
|
|
body, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage source quality read failed: %v", err)}
|
|
}
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return nil, []string{fmt.Sprintf("daily mileage source quality status %d: %s", response.StatusCode, strings.TrimSpace(string(body)))}
|
|
}
|
|
var payload struct {
|
|
Items []capacity.DailyMileageSourceQualityItem `json:"items"`
|
|
Total int64 `json:"total"`
|
|
}
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage source quality decode failed: %v", err)}
|
|
}
|
|
diagnostics := &capacity.DailyMileageSourceQualityDiagnostics{
|
|
URL: requestURL,
|
|
SummaryTotal: payload.Total,
|
|
Items: payload.Items,
|
|
ItemsTruncated: payload.Total > int64(len(payload.Items)),
|
|
}
|
|
for _, item := range payload.Items {
|
|
diagnostics.SourceCount += item.SourceCount
|
|
diagnostics.VehicleCount += item.VehicleCount
|
|
diagnostics.SampleCount += item.SampleCount
|
|
if strings.EqualFold(strings.TrimSpace(item.QualityReason), "current_day_fallback_after_invalid_baseline") {
|
|
diagnostics.FallbackAfterInvalidBaselineSourceCount += item.SourceCount
|
|
}
|
|
if strings.EqualFold(strings.TrimSpace(item.QualityReason), "realtime_location_fallback_historical_baseline") {
|
|
diagnostics.RealtimeLocationFallbackSourceCount += item.SourceCount
|
|
}
|
|
switch strings.ToUpper(strings.TrimSpace(item.QualityStatus)) {
|
|
case "OK":
|
|
diagnostics.OKSourceCount += item.SourceCount
|
|
case "INVALID_DELTA":
|
|
diagnostics.InvalidDeltaSourceCount += item.SourceCount
|
|
case "NO_PREVIOUS_BASELINE":
|
|
diagnostics.NoBaselineSourceCount += item.SourceCount
|
|
}
|
|
}
|
|
if limit <= 0 || len(diagnostics.Items) == 0 {
|
|
diagnostics.Items = nil
|
|
}
|
|
return diagnostics, nil
|
|
}
|
|
|
|
func collectDailyMileageSourceSelection(ctx context.Context, rawURL string, date string, timeout time.Duration, limit int, sampleLimit int) (*capacity.DailyMileageSourceSelectionDiagnostics, []string) {
|
|
requestURL, err := dailyMileageSourceSelectionRequestURL(rawURL, date, limit)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage source selection request invalid: %v", err)}
|
|
}
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage source selection request invalid: %v", err)}
|
|
}
|
|
response, err := (&http.Client{Timeout: timeout}).Do(request)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage source selection fetch failed: %v", err)}
|
|
}
|
|
defer response.Body.Close()
|
|
body, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage source selection read failed: %v", err)}
|
|
}
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return nil, []string{fmt.Sprintf("daily mileage source selection status %d: %s", response.StatusCode, strings.TrimSpace(string(body)))}
|
|
}
|
|
var payload struct {
|
|
Items []capacity.DailyMileageSourceSelectionItem `json:"items"`
|
|
Total int64 `json:"total"`
|
|
}
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage source selection decode failed: %v", err)}
|
|
}
|
|
diagnostics := &capacity.DailyMileageSourceSelectionDiagnostics{
|
|
URL: requestURL,
|
|
SummaryTotal: payload.Total,
|
|
Items: payload.Items,
|
|
ItemsTruncated: payload.Total > int64(len(payload.Items)),
|
|
}
|
|
protocols := map[string]capacity.DailyMileageSourceSelectionProtocol{}
|
|
for _, item := range payload.Items {
|
|
diagnostics.SourceCount += item.SourceCount
|
|
diagnostics.VehicleCount += item.VehicleCount
|
|
diagnostics.SampleCount += item.SampleCount
|
|
protocolName := strings.ToUpper(strings.TrimSpace(item.Protocol))
|
|
protocolSummary := protocols[protocolName]
|
|
protocolSummary.Protocol = protocolName
|
|
protocolSummary.SourceCount += item.SourceCount
|
|
protocolSummary.VehicleCount += item.VehicleCount
|
|
protocolSummary.SampleCount += item.SampleCount
|
|
switch strings.ToLower(strings.TrimSpace(item.SelectionStatus)) {
|
|
case "selected":
|
|
diagnostics.SelectedSourceCount += item.SourceCount
|
|
protocolSummary.SelectedSourceCount += item.SourceCount
|
|
case "excluded":
|
|
diagnostics.ExcludedSourceCount += item.SourceCount
|
|
protocolSummary.ExcludedSourceCount += item.SourceCount
|
|
default:
|
|
diagnostics.NotSelectedSourceCount += item.SourceCount
|
|
diagnostics.NotSelectedMileageKM += item.DailyMileageKM
|
|
protocolSummary.NotSelectedSourceCount += item.SourceCount
|
|
protocolSummary.NotSelectedMileageKM += item.DailyMileageKM
|
|
}
|
|
reason := strings.ToLower(strings.TrimSpace(item.SelectionReason))
|
|
if strings.Contains(reason, "unmanaged") {
|
|
diagnostics.UnmanagedSourceCount += item.SourceCount
|
|
protocolSummary.UnmanagedSourceCount += item.SourceCount
|
|
}
|
|
if strings.Contains(reason, "unknown_source_kind") {
|
|
diagnostics.UnknownKindSourceCount += item.SourceCount
|
|
protocolSummary.UnknownKindSourceCount += item.SourceCount
|
|
}
|
|
if protocolName != "" {
|
|
protocols[protocolName] = protocolSummary
|
|
}
|
|
}
|
|
diagnostics.Protocols = dailyMileageSourceSelectionProtocolSummaries(protocols)
|
|
if limit <= 0 || len(diagnostics.Items) == 0 {
|
|
diagnostics.Items = nil
|
|
}
|
|
var findings []string
|
|
if sampleLimit > 0 && (diagnostics.UnmanagedSourceCount > 0 || diagnostics.UnknownKindSourceCount > 0 || diagnostics.NotSelectedMileageKM > 0) {
|
|
samples, sampleFindings := collectDailyMileageSourceSelectionSamples(ctx, rawURL, date, timeout, sampleLimit)
|
|
diagnostics.Samples = samples
|
|
findings = append(findings, sampleFindings...)
|
|
}
|
|
return diagnostics, findings
|
|
}
|
|
|
|
func dailyMileageSourceSelectionProtocolSummaries(values map[string]capacity.DailyMileageSourceSelectionProtocol) []capacity.DailyMileageSourceSelectionProtocol {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
protocols := make([]string, 0, len(values))
|
|
for protocol := range values {
|
|
protocols = append(protocols, protocol)
|
|
}
|
|
sort.Strings(protocols)
|
|
out := make([]capacity.DailyMileageSourceSelectionProtocol, 0, len(protocols))
|
|
for _, protocol := range protocols {
|
|
out = append(out, values[protocol])
|
|
}
|
|
return out
|
|
}
|
|
|
|
func collectDailyMileageSourceSelectionSamples(ctx context.Context, rawURL string, date string, timeout time.Duration, sampleLimit int) ([]capacity.DailyMileageSourceSelectionSample, []string) {
|
|
fetchLimit := dailyMileageSourceSelectionSampleFetchLimit(sampleLimit)
|
|
requestURL, err := dailyMileageSourceSelectionSampleRequestURL(rawURL, date, fetchLimit)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage source selection sample request invalid: %v", err)}
|
|
}
|
|
samples, err := fetchDailyMileageSourceSelectionSamples(ctx, requestURL, timeout)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("daily mileage source selection sample fetch failed: %v", err)}
|
|
}
|
|
out := make([]capacity.DailyMileageSourceSelectionSample, 0, sampleLimit)
|
|
for _, sample := range samples {
|
|
if !isProblematicDailyMileageSourceSelectionSample(sample) {
|
|
continue
|
|
}
|
|
out = append(out, sample)
|
|
if len(out) >= sampleLimit {
|
|
break
|
|
}
|
|
}
|
|
findings := enrichDailyMileageSourceSelectionSamples(ctx, rawURL, date, timeout, out)
|
|
return out, findings
|
|
}
|
|
|
|
func enrichDailyMileageSourceSelectionSamples(ctx context.Context, rawURL string, date string, timeout time.Duration, samples []capacity.DailyMileageSourceSelectionSample) []string {
|
|
var findings []string
|
|
for index := range samples {
|
|
requestURL, err := dailyMileageSelectedSourceSampleRequestURL(rawURL, date, samples[index], 1)
|
|
if err != nil {
|
|
findings = append(findings, fmt.Sprintf("daily mileage selected source sample request invalid for vin %s: %v", samples[index].VIN, err))
|
|
continue
|
|
}
|
|
samples[index].SelectedSourceQueryPath = requestPathWithQuery(requestURL)
|
|
selected, err := fetchDailyMileageSourceSelectionSamples(ctx, requestURL, timeout)
|
|
if err != nil {
|
|
findings = append(findings, fmt.Sprintf("daily mileage selected source sample fetch failed for vin %s: %v", samples[index].VIN, err))
|
|
continue
|
|
}
|
|
if len(selected) == 0 {
|
|
continue
|
|
}
|
|
samples[index].SelectedSource = &selected[0]
|
|
}
|
|
return findings
|
|
}
|
|
|
|
func fetchDailyMileageSourceSelectionSamples(ctx context.Context, requestURL string, timeout time.Duration) ([]capacity.DailyMileageSourceSelectionSample, error) {
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
response, err := (&http.Client{Timeout: timeout}).Do(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer response.Body.Close()
|
|
body, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return nil, fmt.Errorf("status %d: %s", response.StatusCode, strings.TrimSpace(string(body)))
|
|
}
|
|
var payload struct {
|
|
Items []capacity.DailyMileageSourceSelectionSample `json:"items"`
|
|
}
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
return nil, err
|
|
}
|
|
return payload.Items, nil
|
|
}
|
|
|
|
func isProblematicDailyMileageSourceSelectionSample(sample capacity.DailyMileageSourceSelectionSample) bool {
|
|
reason := strings.ToLower(strings.TrimSpace(sample.SelectionReason))
|
|
status := strings.ToLower(strings.TrimSpace(sample.SelectionStatus))
|
|
return strings.Contains(reason, "unmanaged") ||
|
|
strings.Contains(reason, "unknown_source_kind") ||
|
|
(status == "not_selected" && sample.DailyMileageKM > 0)
|
|
}
|
|
|
|
func dailyMileageSourceSelectionSampleFetchLimit(sampleLimit int) int {
|
|
if sampleLimit <= 0 {
|
|
return 0
|
|
}
|
|
fetchLimit := sampleLimit * 20
|
|
if fetchLimit < sampleLimit {
|
|
fetchLimit = sampleLimit
|
|
}
|
|
if fetchLimit > 1000 {
|
|
return 1000
|
|
}
|
|
return fetchLimit
|
|
}
|
|
|
|
func collectDataSourceDiagnostics(ctx context.Context, rawURL string, timeout time.Duration, sampleLimit int, reasonLimit int, recentAgeSeconds int64) (*capacity.DataSourceDiagnostics, []string) {
|
|
fetchLimit := dataSourceDiagnosticsFetchLimit(sampleLimit, reasonLimit)
|
|
requestURL, err := dataSourceDiagnosticsRequestURL(rawURL, fetchLimit)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("data source diagnostics request invalid: %v", err)}
|
|
}
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("data source diagnostics request invalid: %v", err)}
|
|
}
|
|
response, err := (&http.Client{Timeout: timeout}).Do(request)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("data source diagnostics fetch failed: %v", err)}
|
|
}
|
|
defer response.Body.Close()
|
|
body, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("data source diagnostics read failed: %v", err)}
|
|
}
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return nil, []string{fmt.Sprintf("data source diagnostics status %d: %s", response.StatusCode, strings.TrimSpace(string(body)))}
|
|
}
|
|
var payload struct {
|
|
Items []capacity.DataSourceDiagnosticSample `json:"items"`
|
|
Total int64 `json:"total"`
|
|
}
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
return nil, []string{fmt.Sprintf("data source diagnostics decode failed: %v", err)}
|
|
}
|
|
reasonCounts, actionableCandidates := summarizeDataSourceDiagnosticReasons(payload.Items)
|
|
recentMissing := countRecentDataSourceDiagnostics(payload.Items, recentAgeSeconds)
|
|
samples := payload.Items
|
|
if sampleLimit <= 0 || len(samples) == 0 {
|
|
samples = nil
|
|
} else if len(samples) > sampleLimit {
|
|
samples = samples[:sampleLimit]
|
|
}
|
|
if reasonLimit <= 0 {
|
|
reasonCounts = nil
|
|
actionableCandidates = 0
|
|
}
|
|
diagnostics := &capacity.DataSourceDiagnostics{
|
|
URL: requestURL,
|
|
MissingSourceCodeTotal: payload.Total,
|
|
RecentMissingSourceCodeTotal: recentMissing,
|
|
RecentAgeThresholdSeconds: normalizedRecentDataSourceAgeThreshold(recentAgeSeconds),
|
|
ActionableCandidateTotal: actionableCandidates,
|
|
ReasonCounts: reasonCounts,
|
|
ReasonCountsTruncated: reasonLimit > 0 && payload.Total > int64(len(payload.Items)),
|
|
Samples: samples,
|
|
}
|
|
|
|
mappingDiagnostics, mappingFindings := collectDataSourceMappingIssueDiagnostics(ctx, rawURL, timeout, sampleLimit, reasonLimit, recentAgeSeconds)
|
|
if mappingDiagnostics != nil {
|
|
diagnostics.MappingIssueTotal = mappingDiagnostics.MappingIssueTotal
|
|
diagnostics.RecentMappingIssueTotal = mappingDiagnostics.RecentMappingIssueTotal
|
|
diagnostics.MappingIssueReasonCounts = mappingDiagnostics.MappingIssueReasonCounts
|
|
diagnostics.MappingIssueReasonCountsTruncated = mappingDiagnostics.MappingIssueReasonCountsTruncated
|
|
diagnostics.MappingIssueSamples = mappingDiagnostics.MappingIssueSamples
|
|
}
|
|
return diagnostics, mappingFindings
|
|
}
|
|
|
|
func collectDataSourceMappingIssueDiagnostics(ctx context.Context, rawURL string, timeout time.Duration, sampleLimit int, reasonLimit int, recentAgeSeconds int64) (*capacity.DataSourceDiagnostics, []string) {
|
|
fetchLimit := dataSourceDiagnosticsFetchLimit(sampleLimit, reasonLimit)
|
|
requestURL, err := dataSourceMappingIssuesRequestURL(rawURL, fetchLimit)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("data source mapping diagnostics request invalid: %v", err)}
|
|
}
|
|
payload, findings := fetchDataSourceDiagnosticPage(ctx, requestURL, timeout, "data source mapping diagnostics")
|
|
if len(findings) > 0 {
|
|
return nil, findings
|
|
}
|
|
reasonCounts, _ := summarizeDataSourceDiagnosticReasons(payload.Items)
|
|
recentMappingIssues := countRecentDataSourceDiagnostics(payload.Items, recentAgeSeconds)
|
|
samples := payload.Items
|
|
if sampleLimit <= 0 || len(samples) == 0 {
|
|
samples = nil
|
|
} else if len(samples) > sampleLimit {
|
|
samples = samples[:sampleLimit]
|
|
}
|
|
if reasonLimit <= 0 {
|
|
reasonCounts = nil
|
|
}
|
|
return &capacity.DataSourceDiagnostics{
|
|
MappingIssueTotal: payload.Total,
|
|
RecentMappingIssueTotal: recentMappingIssues,
|
|
RecentAgeThresholdSeconds: normalizedRecentDataSourceAgeThreshold(recentAgeSeconds),
|
|
MappingIssueReasonCounts: reasonCounts,
|
|
MappingIssueReasonCountsTruncated: reasonLimit > 0 && payload.Total > int64(len(payload.Items)),
|
|
MappingIssueSamples: samples,
|
|
}, nil
|
|
}
|
|
|
|
func collectDataSourceKindSuggestions(ctx context.Context, rawURL string, timeout time.Duration, protocols []string, sampleLimit int, reasonLimit int, recentAgeSeconds int64) (*capacity.DataSourceDiagnostics, []string) {
|
|
fetchLimit := dataSourceDiagnosticsFetchLimit(sampleLimit, reasonLimit)
|
|
if len(protocols) == 0 {
|
|
protocols = parseDataSourceKindSuggestionProtocols(defaultDataSourceKindSuggestionProtocols)
|
|
}
|
|
var findings []string
|
|
var samples []capacity.DataSourceDiagnosticSample
|
|
var unknownTotal int64
|
|
var recentUnknown int64
|
|
var platformCandidates int64
|
|
var recentPlatformCandidates int64
|
|
reasonCounts := map[string]int64{}
|
|
reasonSummaryEnabled := reasonLimit > 0
|
|
reasonCountsTruncated := false
|
|
var firstURL string
|
|
for _, protocol := range protocols {
|
|
requestURL, err := dataSourceKindSuggestionsRequestURL(rawURL, protocol, fetchLimit)
|
|
if err != nil {
|
|
findings = append(findings, fmt.Sprintf("data source kind suggestions request invalid for %s: %v", protocol, err))
|
|
continue
|
|
}
|
|
if firstURL == "" {
|
|
firstURL = requestURL
|
|
}
|
|
payload, fetchFindings := fetchDataSourceDiagnosticPage(ctx, requestURL, timeout, "data source kind suggestions")
|
|
if len(fetchFindings) > 0 {
|
|
findings = append(findings, fetchFindings...)
|
|
continue
|
|
}
|
|
unknownTotal += payload.Total
|
|
protocolRecentUnknown := countRecentDataSourceDiagnostics(payload.Items, recentAgeSeconds)
|
|
recentUnknown += protocolRecentUnknown
|
|
protocolReasonCounts, protocolPlatformCandidates, protocolRecentPlatformCandidates := summarizeDataSourceKindSuggestionReasons(payload.Items, recentAgeSeconds)
|
|
platformCandidates += protocolPlatformCandidates
|
|
recentPlatformCandidates += protocolRecentPlatformCandidates
|
|
if reasonSummaryEnabled {
|
|
mergeInt64Counts(reasonCounts, protocolReasonCounts)
|
|
reasonCountsTruncated = reasonCountsTruncated || payload.Total > int64(len(payload.Items))
|
|
}
|
|
if sampleLimit > 0 && len(samples) < sampleLimit {
|
|
for _, item := range payload.Items {
|
|
if !isPlatformKindCandidate(item) {
|
|
continue
|
|
}
|
|
samples = append(samples, item)
|
|
if len(samples) >= sampleLimit {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !reasonSummaryEnabled || len(reasonCounts) == 0 {
|
|
reasonCounts = nil
|
|
}
|
|
if sampleLimit <= 0 || len(samples) == 0 {
|
|
samples = nil
|
|
}
|
|
return &capacity.DataSourceDiagnostics{
|
|
URL: firstURL,
|
|
UnknownSourceKindTotal: unknownTotal,
|
|
RecentUnknownSourceKindTotal: recentUnknown,
|
|
RecentAgeThresholdSeconds: normalizedRecentDataSourceAgeThreshold(recentAgeSeconds),
|
|
PlatformKindCandidateTotal: platformCandidates,
|
|
RecentPlatformKindCandidateTotal: recentPlatformCandidates,
|
|
ReasonCounts: reasonCounts,
|
|
ReasonCountsTruncated: reasonCountsTruncated,
|
|
Samples: samples,
|
|
}, findings
|
|
}
|
|
|
|
func collectJT808IdentityGaps(ctx context.Context, rawURL string, timeout time.Duration, recentSeconds int64, sampleLimit int) (*capacity.JT808IdentityGapDiagnostics, []string) {
|
|
requestURL, err := jt808IdentityGapsRequestURL(rawURL, recentSeconds, sampleLimit)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("JT808 identity gaps request invalid: %v", err)}
|
|
}
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("JT808 identity gaps request invalid: %v", err)}
|
|
}
|
|
response, err := (&http.Client{Timeout: timeout}).Do(request)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("JT808 identity gaps fetch failed: %v", err)}
|
|
}
|
|
defer response.Body.Close()
|
|
body, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
return nil, []string{fmt.Sprintf("JT808 identity gaps read failed: %v", err)}
|
|
}
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return nil, []string{fmt.Sprintf("JT808 identity gaps status %d: %s", response.StatusCode, strings.TrimSpace(string(body)))}
|
|
}
|
|
var payload struct {
|
|
Items []capacity.JT808IdentityGapSample `json:"items"`
|
|
Total int64 `json:"total"`
|
|
RecentSeconds int64 `json:"recentSeconds"`
|
|
}
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
return nil, []string{fmt.Sprintf("JT808 identity gaps decode failed: %v", err)}
|
|
}
|
|
samples := payload.Items
|
|
if sampleLimit <= 0 || len(samples) == 0 {
|
|
samples = nil
|
|
} else if len(samples) > sampleLimit {
|
|
samples = samples[:sampleLimit]
|
|
}
|
|
return &capacity.JT808IdentityGapDiagnostics{
|
|
URL: requestURL,
|
|
Total: payload.Total,
|
|
RecentSeconds: payload.RecentSeconds,
|
|
Samples: samples,
|
|
}, nil
|
|
}
|
|
|
|
type dataSourceDiagnosticPayload struct {
|
|
Items []capacity.DataSourceDiagnosticSample `json:"items"`
|
|
Total int64 `json:"total"`
|
|
}
|
|
|
|
func fetchDataSourceDiagnosticPage(ctx context.Context, requestURL string, timeout time.Duration, label string) (dataSourceDiagnosticPayload, []string) {
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
|
if err != nil {
|
|
return dataSourceDiagnosticPayload{}, []string{fmt.Sprintf("%s request invalid: %v", label, err)}
|
|
}
|
|
response, err := (&http.Client{Timeout: timeout}).Do(request)
|
|
if err != nil {
|
|
return dataSourceDiagnosticPayload{}, []string{fmt.Sprintf("%s fetch failed: %v", label, err)}
|
|
}
|
|
defer response.Body.Close()
|
|
body, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
return dataSourceDiagnosticPayload{}, []string{fmt.Sprintf("%s read failed: %v", label, err)}
|
|
}
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return dataSourceDiagnosticPayload{}, []string{fmt.Sprintf("%s status %d: %s", label, response.StatusCode, strings.TrimSpace(string(body)))}
|
|
}
|
|
var payload dataSourceDiagnosticPayload
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
return dataSourceDiagnosticPayload{}, []string{fmt.Sprintf("%s decode failed: %v", label, err)}
|
|
}
|
|
return payload, nil
|
|
}
|
|
|
|
func dataSourceDiagnosticsFetchLimit(sampleLimit int, reasonLimit int) int {
|
|
limit := sampleLimit
|
|
if reasonLimit > limit {
|
|
limit = reasonLimit
|
|
}
|
|
if limit < 0 {
|
|
return 0
|
|
}
|
|
return limit
|
|
}
|
|
|
|
func summarizeDataSourceDiagnosticReasons(items []capacity.DataSourceDiagnosticSample) (map[string]int64, int64) {
|
|
if len(items) == 0 {
|
|
return nil, 0
|
|
}
|
|
counts := make(map[string]int64)
|
|
var actionable int64
|
|
for _, item := range items {
|
|
reason := strings.TrimSpace(item.Reason)
|
|
if reason == "" {
|
|
reason = "unknown"
|
|
}
|
|
counts[reason]++
|
|
if reason == "candidate_available" {
|
|
actionable++
|
|
}
|
|
}
|
|
return counts, actionable
|
|
}
|
|
|
|
func countRecentDataSourceDiagnostics(items []capacity.DataSourceDiagnosticSample, recentAgeSeconds int64) int64 {
|
|
if recentAgeSeconds <= 0 {
|
|
return 0
|
|
}
|
|
var count int64
|
|
for _, item := range items {
|
|
if item.LatestSeenAgeSeconds <= recentAgeSeconds {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func summarizeDataSourceKindSuggestionReasons(items []capacity.DataSourceDiagnosticSample, recentAgeSeconds int64) (map[string]int64, int64, int64) {
|
|
if len(items) == 0 {
|
|
return nil, 0, 0
|
|
}
|
|
counts := make(map[string]int64)
|
|
var platformCandidates int64
|
|
var recentPlatformCandidates int64
|
|
for _, item := range items {
|
|
reason := strings.TrimSpace(item.SuggestionReason)
|
|
if reason == "" {
|
|
reason = strings.TrimSpace(item.Reason)
|
|
}
|
|
if reason == "" {
|
|
reason = "unknown"
|
|
}
|
|
counts[reason]++
|
|
if !isPlatformKindCandidate(item) {
|
|
continue
|
|
}
|
|
platformCandidates++
|
|
if recentAgeSeconds > 0 && item.LatestSeenAgeSeconds <= recentAgeSeconds {
|
|
recentPlatformCandidates++
|
|
}
|
|
}
|
|
return counts, platformCandidates, recentPlatformCandidates
|
|
}
|
|
|
|
func isPlatformKindCandidate(item capacity.DataSourceDiagnosticSample) bool {
|
|
if !strings.EqualFold(strings.TrimSpace(item.SuggestedSourceKind), "PLATFORM") {
|
|
return false
|
|
}
|
|
return !strings.EqualFold(strings.TrimSpace(item.SuggestionConfidence), "LOW")
|
|
}
|
|
|
|
func mergeInt64Counts(dst map[string]int64, src map[string]int64) {
|
|
for key, value := range src {
|
|
if key == "" || value == 0 {
|
|
continue
|
|
}
|
|
dst[key] += value
|
|
}
|
|
}
|
|
|
|
func normalizedRecentDataSourceAgeThreshold(recentAgeSeconds int64) int64 {
|
|
if recentAgeSeconds <= 0 {
|
|
return 0
|
|
}
|
|
return recentAgeSeconds
|
|
}
|
|
|
|
func collectDailyMileageDiagnosisSamples(ctx context.Context, rawURL string, date string, timeout time.Duration, items []capacity.DailyMileageDiagnosisReasonItem, sampleLimit int) ([]capacity.DailyMileageDiagnosisSample, []string) {
|
|
if sampleLimit <= 0 {
|
|
return nil, nil
|
|
}
|
|
var samples []capacity.DailyMileageDiagnosisSample
|
|
var findings []string
|
|
seen := make(map[string]struct{})
|
|
for _, item := range items {
|
|
if len(samples) >= sampleLimit {
|
|
break
|
|
}
|
|
if capacity.DailyMileageIssueSeverity(item) == capacity.DailyMileageSeverityOK || item.Count <= 0 {
|
|
continue
|
|
}
|
|
key := strings.ToUpper(strings.TrimSpace(item.Protocol)) + "|" + strings.ToUpper(strings.TrimSpace(item.Diagnosis))
|
|
if _, exists := seen[key]; exists {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
requestURL, err := dailyMileageDiagnosticsSampleRequestURL(rawURL, date, item, sampleLimit-len(samples))
|
|
if err != nil {
|
|
findings = append(findings, fmt.Sprintf("daily mileage diagnostics sample request invalid: %v", err))
|
|
continue
|
|
}
|
|
fetched, err := fetchDailyMileageDiagnosisSamples(ctx, requestURL, timeout)
|
|
if err != nil {
|
|
findings = append(findings, fmt.Sprintf("daily mileage diagnostics sample fetch failed: %v", err))
|
|
continue
|
|
}
|
|
for _, sample := range fetched {
|
|
if len(samples) >= sampleLimit {
|
|
break
|
|
}
|
|
sample.Severity = capacity.DailyMileageIssueSeverity(capacity.DailyMileageDiagnosisReasonItem{
|
|
Diagnosis: sample.Diagnosis,
|
|
Reason: sample.Reason,
|
|
Count: 1,
|
|
})
|
|
if strings.TrimSpace(sample.RecommendedOperatorAction) == "" {
|
|
sample.RecommendedOperatorAction = capacity.DailyMileageRecommendedOperatorAction(sample.Diagnosis, sample.Reason)
|
|
}
|
|
samples = append(samples, sample)
|
|
}
|
|
}
|
|
return samples, findings
|
|
}
|
|
|
|
func fetchDailyMileageDiagnosisSamples(ctx context.Context, requestURL string, timeout time.Duration) ([]capacity.DailyMileageDiagnosisSample, error) {
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
response, err := (&http.Client{Timeout: timeout}).Do(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer response.Body.Close()
|
|
body, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return nil, fmt.Errorf("status %d: %s", response.StatusCode, strings.TrimSpace(string(body)))
|
|
}
|
|
var payload struct {
|
|
Items []capacity.DailyMileageDiagnosisSample `json:"items"`
|
|
}
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
return nil, err
|
|
}
|
|
return payload.Items, nil
|
|
}
|
|
|
|
func dailyMileageDiagnosticsRequestURL(rawURL string, date string) (string, error) {
|
|
parsed, err := url.Parse(strings.TrimSpace(rawURL))
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
return "", fmt.Errorf("invalid URL %q", rawURL)
|
|
}
|
|
if parsed.Path == "" || parsed.Path == "/" {
|
|
parsed.Path = defaultDailyMileageDiagnosticsPath
|
|
}
|
|
query := parsed.Query()
|
|
if date = strings.TrimSpace(date); date != "" {
|
|
query.Set("date", date)
|
|
}
|
|
parsed.RawQuery = query.Encode()
|
|
return parsed.String(), nil
|
|
}
|
|
|
|
func dailyMileageFieldStatusRequestURL(rawURL string, date string) (string, error) {
|
|
parsed, err := url.Parse(strings.TrimSpace(rawURL))
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
return "", fmt.Errorf("invalid URL %q", rawURL)
|
|
}
|
|
switch {
|
|
case parsed.Path == "" || parsed.Path == "/":
|
|
parsed.Path = defaultDailyMileageFieldStatusPath
|
|
case strings.HasSuffix(parsed.Path, "/reasons"):
|
|
parsed.Path = strings.TrimSuffix(parsed.Path, "/reasons") + "/field-status"
|
|
case strings.HasSuffix(parsed.Path, "/summary"):
|
|
parsed.Path = strings.TrimSuffix(parsed.Path, "/summary") + "/field-status"
|
|
case strings.HasSuffix(parsed.Path, "/diagnostics"):
|
|
parsed.Path = parsed.Path + "/field-status"
|
|
case strings.HasSuffix(parsed.Path, "/field-status"):
|
|
default:
|
|
parsed.Path = defaultDailyMileageFieldStatusPath
|
|
}
|
|
query := parsed.Query()
|
|
if date = strings.TrimSpace(date); date != "" {
|
|
query.Set("date", date)
|
|
}
|
|
parsed.RawQuery = query.Encode()
|
|
return parsed.String(), nil
|
|
}
|
|
|
|
func dailyMileageSourceQualityRequestURL(rawURL string, date string, limit int) (string, error) {
|
|
parsed, err := url.Parse(strings.TrimSpace(rawURL))
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
return "", fmt.Errorf("invalid URL %q", rawURL)
|
|
}
|
|
if parsed.Path == "" || parsed.Path == "/" {
|
|
parsed.Path = defaultDailyMileageSourceQualityPath
|
|
}
|
|
limit = normalizedPositiveLimit(limit)
|
|
statDate := dailyMileageSourceQualityDate(date)
|
|
query := parsed.Query()
|
|
query.Set("dateFrom", statDate)
|
|
query.Set("dateTo", statDate)
|
|
query.Set("includeTotal", "true")
|
|
query.Set("limit", fmt.Sprintf("%d", limit))
|
|
query.Set("offset", "0")
|
|
parsed.RawQuery = query.Encode()
|
|
return parsed.String(), nil
|
|
}
|
|
|
|
func dailyMileageSourceSelectionRequestURL(rawURL string, date string, limit int) (string, error) {
|
|
parsed, err := url.Parse(strings.TrimSpace(rawURL))
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
return "", fmt.Errorf("invalid URL %q", rawURL)
|
|
}
|
|
if parsed.Path == "" || parsed.Path == "/" {
|
|
parsed.Path = defaultDailyMileageSourceSelectionPath
|
|
}
|
|
limit = normalizedPositiveLimit(limit)
|
|
statDate := dailyMileageSourceQualityDate(date)
|
|
query := parsed.Query()
|
|
query.Set("dateFrom", statDate)
|
|
query.Set("dateTo", statDate)
|
|
query.Set("includeTotal", "true")
|
|
query.Set("limit", fmt.Sprintf("%d", limit))
|
|
query.Set("offset", "0")
|
|
parsed.RawQuery = query.Encode()
|
|
return parsed.String(), nil
|
|
}
|
|
|
|
func dailyMileageSourceSelectionSampleRequestURL(rawURL string, date string, limit int) (string, error) {
|
|
parsed, err := url.Parse(strings.TrimSpace(rawURL))
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
return "", fmt.Errorf("invalid URL %q", rawURL)
|
|
}
|
|
path := strings.TrimRight(parsed.Path, "/")
|
|
switch {
|
|
case path == "":
|
|
parsed.Path = defaultDailyMileageSourcesPath
|
|
case strings.HasSuffix(path, "/selection"):
|
|
parsed.Path = strings.TrimSuffix(path, "/selection")
|
|
case strings.HasSuffix(path, "/quality"):
|
|
parsed.Path = strings.TrimSuffix(path, "/quality")
|
|
case path == defaultDailyMileageSourcesPath:
|
|
parsed.Path = defaultDailyMileageSourcesPath
|
|
default:
|
|
parsed.Path = defaultDailyMileageSourcesPath
|
|
}
|
|
limit = normalizedPositiveLimit(limit)
|
|
statDate := dailyMileageSourceQualityDate(date)
|
|
query := parsed.Query()
|
|
query.Set("dateFrom", statDate)
|
|
query.Set("dateTo", statDate)
|
|
query.Set("selected", "false")
|
|
query.Set("includeTotal", "false")
|
|
query.Set("orderBy", "dailyMileageDesc")
|
|
query.Set("limit", fmt.Sprintf("%d", limit))
|
|
query.Set("offset", "0")
|
|
parsed.RawQuery = query.Encode()
|
|
return parsed.String(), nil
|
|
}
|
|
|
|
func dailyMileageSelectedSourceSampleRequestURL(rawURL string, date string, sample capacity.DailyMileageSourceSelectionSample, limit int) (string, error) {
|
|
parsed, err := url.Parse(strings.TrimSpace(rawURL))
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
return "", fmt.Errorf("invalid URL %q", rawURL)
|
|
}
|
|
parsed.Path = defaultDailyMileageSourcesPath
|
|
limit = normalizedPositiveLimit(limit)
|
|
statDate := dailyMileageSourceQualityDate(date)
|
|
query := parsed.Query()
|
|
query.Set("dateFrom", statDate)
|
|
query.Set("dateTo", statDate)
|
|
query.Set("selected", "true")
|
|
query.Set("includeTotal", "false")
|
|
query.Set("limit", fmt.Sprintf("%d", limit))
|
|
query.Set("offset", "0")
|
|
if vin := strings.TrimSpace(sample.VIN); vin != "" {
|
|
query.Set("vin", vin)
|
|
}
|
|
if protocol := strings.TrimSpace(sample.Protocol); protocol != "" {
|
|
query.Set("protocol", protocol)
|
|
}
|
|
parsed.RawQuery = query.Encode()
|
|
return parsed.String(), nil
|
|
}
|
|
|
|
func requestPathWithQuery(requestURL string) string {
|
|
parsed, err := url.Parse(requestURL)
|
|
if err != nil {
|
|
return requestURL
|
|
}
|
|
if parsed.RawQuery == "" {
|
|
return parsed.EscapedPath()
|
|
}
|
|
return parsed.EscapedPath() + "?" + parsed.RawQuery
|
|
}
|
|
|
|
func dailyMileageSourceQualityDate(date string) string {
|
|
if date = strings.TrimSpace(date); date != "" {
|
|
return date
|
|
}
|
|
return time.Now().In(time.FixedZone("Asia/Shanghai", 8*3600)).Format("2006-01-02")
|
|
}
|
|
|
|
func normalizedPositiveLimit(limit int) int {
|
|
if limit <= 0 {
|
|
return 1
|
|
}
|
|
return limit
|
|
}
|
|
|
|
func dataSourceDiagnosticsRequestURL(rawURL string, sampleLimit int) (string, error) {
|
|
parsed, err := url.Parse(strings.TrimSpace(rawURL))
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
return "", fmt.Errorf("invalid URL %q", rawURL)
|
|
}
|
|
if parsed.Path == "" || parsed.Path == "/" {
|
|
parsed.Path = defaultDataSourceDiagnosticsPath
|
|
}
|
|
limit := normalizedPositiveLimit(sampleLimit)
|
|
query := parsed.Query()
|
|
query.Set("sourceCodeMissing", "true")
|
|
query.Set("includeTotal", "true")
|
|
query.Set("limit", fmt.Sprintf("%d", limit))
|
|
query.Set("offset", "0")
|
|
parsed.RawQuery = query.Encode()
|
|
return parsed.String(), nil
|
|
}
|
|
|
|
func dataSourceMappingIssuesRequestURL(rawURL string, sampleLimit int) (string, error) {
|
|
parsed, err := url.Parse(strings.TrimSpace(rawURL))
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
return "", fmt.Errorf("invalid URL %q", rawURL)
|
|
}
|
|
if parsed.Path == "" || parsed.Path == "/" {
|
|
parsed.Path = defaultDataSourceDiagnosticsPath
|
|
}
|
|
limit := normalizedPositiveLimit(sampleLimit)
|
|
query := parsed.Query()
|
|
query.Set("sourceCodeMissing", "false")
|
|
query.Set("mappingIssueOnly", "true")
|
|
query.Set("includeTotal", "true")
|
|
query.Set("limit", fmt.Sprintf("%d", limit))
|
|
query.Set("offset", "0")
|
|
parsed.RawQuery = query.Encode()
|
|
return parsed.String(), nil
|
|
}
|
|
|
|
func dataSourceKindSuggestionsRequestURL(rawURL string, protocol string, sampleLimit int) (string, error) {
|
|
parsed, err := url.Parse(strings.TrimSpace(rawURL))
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
return "", fmt.Errorf("invalid URL %q", rawURL)
|
|
}
|
|
if parsed.Path == "" || parsed.Path == "/" {
|
|
parsed.Path = defaultDataSourceKindSuggestionsPath
|
|
}
|
|
limit := normalizedPositiveLimit(sampleLimit)
|
|
query := parsed.Query()
|
|
query.Set("protocol", strings.ToUpper(strings.TrimSpace(protocol)))
|
|
query.Set("sourceKind", "UNKNOWN")
|
|
query.Set("includeTotal", "true")
|
|
query.Set("limit", fmt.Sprintf("%d", limit))
|
|
query.Set("offset", "0")
|
|
parsed.RawQuery = query.Encode()
|
|
return parsed.String(), nil
|
|
}
|
|
|
|
func jt808IdentityGapsRequestURL(rawURL string, recentSeconds int64, sampleLimit int) (string, error) {
|
|
parsed, err := url.Parse(strings.TrimSpace(rawURL))
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
return "", fmt.Errorf("invalid URL %q", rawURL)
|
|
}
|
|
if parsed.Path == "" || parsed.Path == "/" {
|
|
parsed.Path = defaultJT808IdentityGapsPath
|
|
}
|
|
limit := normalizedPositiveLimit(sampleLimit)
|
|
query := parsed.Query()
|
|
query.Set("includeTotal", "true")
|
|
query.Set("limit", fmt.Sprintf("%d", limit))
|
|
query.Set("offset", "0")
|
|
if recentSeconds >= 0 {
|
|
query.Set("recentSeconds", fmt.Sprintf("%d", recentSeconds))
|
|
}
|
|
parsed.RawQuery = query.Encode()
|
|
return parsed.String(), nil
|
|
}
|
|
|
|
func dailyMileageDiagnosticsSampleRequestURL(rawURL string, date string, item capacity.DailyMileageDiagnosisReasonItem, limit int) (string, error) {
|
|
parsed, err := url.Parse(strings.TrimSpace(rawURL))
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
return "", fmt.Errorf("invalid URL %q", rawURL)
|
|
}
|
|
switch {
|
|
case parsed.Path == "" || parsed.Path == "/":
|
|
parsed.Path = "/api/stats/daily-metrics/diagnostics"
|
|
case strings.HasSuffix(parsed.Path, "/reasons"):
|
|
parsed.Path = strings.TrimSuffix(parsed.Path, "/reasons")
|
|
case strings.HasSuffix(parsed.Path, "/summary"):
|
|
parsed.Path = strings.TrimSuffix(parsed.Path, "/summary")
|
|
}
|
|
if !strings.HasSuffix(parsed.Path, "/diagnostics") {
|
|
parsed.Path = "/api/stats/daily-metrics/diagnostics"
|
|
}
|
|
query := parsed.Query()
|
|
query.Del("reason")
|
|
if date == "" {
|
|
date = item.StatDate
|
|
}
|
|
if date != "" {
|
|
query.Set("date", date)
|
|
}
|
|
query.Set("protocol", strings.ToUpper(strings.TrimSpace(item.Protocol)))
|
|
query.Set("diagnosis", strings.ToUpper(strings.TrimSpace(item.Diagnosis)))
|
|
query.Set("limit", fmt.Sprintf("%d", limit))
|
|
query.Set("offset", "0")
|
|
parsed.RawQuery = query.Encode()
|
|
return parsed.String(), nil
|
|
}
|