diagnostics.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. package prom
  2. import (
  3. "fmt"
  4. "time"
  5. "github.com/opencost/opencost/core/pkg/log"
  6. "github.com/opencost/opencost/pkg/env"
  7. prometheus "github.com/prometheus/client_golang/api"
  8. )
  9. // Prometheus Metric Diagnostic IDs
  10. const (
  11. // CAdvisorDiagnosticMetricID is the identifier of the metric used to determine if cAdvisor is being scraped.
  12. CAdvisorDiagnosticMetricID = "cadvisorMetric"
  13. // CAdvisorLabelDiagnosticMetricID is the identifier of the metric used to determine if cAdvisor labels are correct.
  14. CAdvisorLabelDiagnosticMetricID = "cadvisorLabel"
  15. // KSMDiagnosticMetricID is the identifier for the metric used to determine if KSM metrics are being scraped.
  16. KSMDiagnosticMetricID = "ksmMetric"
  17. // KSMVersionDiagnosticMetricID is the identifier for the metric used to determine if KSM version is correct.
  18. KSMVersionDiagnosticMetricID = "ksmVersion"
  19. // KubecostDiagnosticMetricID is the identifier for the metric used to determine if Kubecost metrics are being scraped.
  20. KubecostDiagnosticMetricID = "kubecostMetric"
  21. // NodeExporterDiagnosticMetricID is the identifier for the metric used to determine if NodeExporter metrics are being scraped.
  22. NodeExporterDiagnosticMetricID = "neMetric"
  23. // ScrapeIntervalDiagnosticMetricID is the identifier for the metric used to determine if prometheus has its own self-scraped
  24. // metrics.
  25. ScrapeIntervalDiagnosticMetricID = "scrapeInterval"
  26. // CPUThrottlingDiagnosticMetricID is the identifier for the metric used to determine if CPU throttling is being applied to the
  27. // cost-model container.
  28. CPUThrottlingDiagnosticMetricID = "cpuThrottling"
  29. // KubecostRecordingRuleCPUUsageID is the identifier for the query used to
  30. // determine of the CPU usage recording rule is set up correctly.
  31. KubecostRecordingRuleCPUUsageID = "kubecostRecordingRuleCPUUsage"
  32. // CAdvisorWorkingSetBytesMetricID is the identifier for the query used to determine
  33. // if cAdvisor working set bytes data is being scraped
  34. CAdvisorWorkingSetBytesMetricID = "cadvisorWorkingSetBytesMetric"
  35. // KSMCPUCapacityMetricID is the identifier for the query used to determine if
  36. // KSM CPU capacity data is being scraped
  37. KSMCPUCapacityMetricID = "ksmCpuCapacityMetric"
  38. // KSMAllocatableCPUCoresMetricID is the identifier for the query used to determine
  39. // if KSM allocatable CPU core data is being scraped
  40. KSMAllocatableCPUCoresMetricID = "ksmAllocatableCpuCoresMetric"
  41. )
  42. const DocumentationBaseURL = "https://github.com/kubecost/docs/blob/master/diagnostics.md"
  43. // diagnostic definitions mapping holds all of the diagnostic definitions that can be used for prometheus metrics diagnostics
  44. var diagnosticDefinitions map[string]*diagnosticDefinition = map[string]*diagnosticDefinition{
  45. CAdvisorDiagnosticMetricID: {
  46. ID: CAdvisorDiagnosticMetricID,
  47. QueryFmt: `absent_over_time(container_cpu_usage_seconds_total{%s}[5m] %s)`,
  48. Label: "cAdvisor metrics available",
  49. Description: "Determine if cAdvisor metrics are available during last 5 minutes.",
  50. DocLink: fmt.Sprintf("%s#cadvisor-metrics-available", DocumentationBaseURL),
  51. },
  52. KSMDiagnosticMetricID: {
  53. ID: KSMDiagnosticMetricID,
  54. QueryFmt: `absent_over_time(kube_pod_container_resource_requests{resource="memory", unit="byte", %s}[5m] %s)`,
  55. Label: "Kube-state-metrics available",
  56. Description: "Determine if metrics from kube-state-metrics are available during last 5 minutes.",
  57. DocLink: fmt.Sprintf("%s#kube-state-metrics-metrics-available", DocumentationBaseURL),
  58. },
  59. KubecostDiagnosticMetricID: {
  60. ID: KubecostDiagnosticMetricID,
  61. QueryFmt: `absent_over_time(node_cpu_hourly_cost{%s}[5m] %s)`,
  62. Label: "Kubecost metrics available",
  63. Description: "Determine if metrics from Kubecost are available during last 5 minutes.",
  64. },
  65. NodeExporterDiagnosticMetricID: {
  66. ID: NodeExporterDiagnosticMetricID,
  67. QueryFmt: `absent_over_time(node_cpu_seconds_total{%s}[5m] %s)`,
  68. Label: "Node-exporter metrics available",
  69. Description: "Determine if metrics from node-exporter are available during last 5 minutes.",
  70. DocLink: fmt.Sprintf("%s#node-exporter-metrics-available", DocumentationBaseURL),
  71. },
  72. CAdvisorLabelDiagnosticMetricID: {
  73. ID: CAdvisorLabelDiagnosticMetricID,
  74. QueryFmt: `absent_over_time(container_cpu_usage_seconds_total{container!="",pod!="", %s}[5m] %s)`,
  75. Label: "Expected cAdvisor labels available",
  76. Description: "Determine if expected cAdvisor labels are present during last 5 minutes.",
  77. DocLink: fmt.Sprintf("%s#cadvisor-metrics-available", DocumentationBaseURL),
  78. },
  79. KSMVersionDiagnosticMetricID: {
  80. ID: KSMVersionDiagnosticMetricID,
  81. QueryFmt: `absent_over_time(kube_persistentvolume_capacity_bytes{%s}[5m] %s)`,
  82. Label: "Expected kube-state-metrics version found",
  83. Description: "Determine if metric in required kube-state-metrics version are present during last 5 minutes.",
  84. DocLink: fmt.Sprintf("%s#expected-kube-state-metrics-version-found", DocumentationBaseURL),
  85. },
  86. ScrapeIntervalDiagnosticMetricID: {
  87. ID: ScrapeIntervalDiagnosticMetricID,
  88. QueryFmt: `absent_over_time(prometheus_target_interval_length_seconds{%s}[5m] %s)`,
  89. Label: "Expected Prometheus self-scrape metrics available",
  90. Description: "Determine if prometheus has its own self-scraped metrics during the last 5 minutes.",
  91. },
  92. CPUThrottlingDiagnosticMetricID: {
  93. ID: CPUThrottlingDiagnosticMetricID,
  94. QueryFmt: `avg(increase(container_cpu_cfs_throttled_periods_total{container="cost-model", %s}[10m] %s)) by (container_name, pod_name, namespace)
  95. / avg(increase(container_cpu_cfs_periods_total{container="cost-model",%s}[10m] %s)) by (container_name, pod_name, namespace) > 0.2`,
  96. Label: "Kubecost is not CPU throttled",
  97. Description: "Kubecost loading slowly? A kubecost component might be CPU throttled",
  98. },
  99. KubecostRecordingRuleCPUUsageID: {
  100. ID: KubecostRecordingRuleCPUUsageID,
  101. QueryFmt: `absent_over_time(kubecost_container_cpu_usage_irate{%s}[5m] %s)`,
  102. Label: "Kubecost's CPU usage recording rule is set up",
  103. Description: "If the 'kubecost_container_cpu_usage_irate' recording rule is not set up, Allocation pipeline build may put pressure on your Prometheus due to the use of a subquery.",
  104. DocLink: "https://docs.kubecost.com/install-and-configure/install/custom-prom",
  105. },
  106. CAdvisorWorkingSetBytesMetricID: {
  107. ID: CAdvisorWorkingSetBytesMetricID,
  108. QueryFmt: `absent_over_time(container_memory_working_set_bytes{container="cost-model", container!="POD", instance!="", %s}[5m] %s)`,
  109. Label: "cAdvisor working set bytes metrics available",
  110. Description: "Determine if cAdvisor working set bytes metrics are available during last 5 minutes.",
  111. },
  112. KSMCPUCapacityMetricID: {
  113. ID: KSMCPUCapacityMetricID,
  114. QueryFmt: `absent_over_time(kube_node_status_capacity_cpu_cores{%s}[5m] %s)`,
  115. Label: "KSM had CPU capacity during the last 5 minutes",
  116. Description: "Determine if KSM had CPU capacity during the last 5 minutes",
  117. },
  118. KSMAllocatableCPUCoresMetricID: {
  119. ID: KSMAllocatableCPUCoresMetricID,
  120. QueryFmt: `absent_over_time(kube_node_status_allocatable_cpu_cores{%s}[5m] %s)`,
  121. Label: "KSM had allocatable CPU cores during the last 5 minutes",
  122. Description: "Determine if KSM had allocatable CPU cores during the last 5 minutes",
  123. },
  124. }
  125. // QueuedPromRequest is a representation of a request waiting to be sent by the prometheus
  126. // client.
  127. type QueuedPromRequest struct {
  128. Context string `json:"context"`
  129. Query string `json:"query"`
  130. QueueTime int64 `json:"queueTime"`
  131. }
  132. // PrometheusQueueState contains diagnostic information concerning the state of the prometheus request
  133. // queue
  134. type PrometheusQueueState struct {
  135. QueuedRequests []*QueuedPromRequest `json:"queuedRequests"`
  136. OutboundRequests int `json:"outboundRequests"`
  137. TotalRequests int `json:"totalRequests"`
  138. MaxQueryConcurrency int `json:"maxQueryConcurrency"`
  139. }
  140. // GetPrometheusQueueState is a diagnostic function that probes the prometheus request queue and gathers
  141. // query, context, and queue statistics.
  142. func GetPrometheusQueueState(client prometheus.Client) (*PrometheusQueueState, error) {
  143. rlpc, ok := client.(*RateLimitedPrometheusClient)
  144. if !ok {
  145. return nil, fmt.Errorf("Failed to get prometheus queue state for the provided client. Must be of type RateLimitedPrometheusClient.")
  146. }
  147. outbound := rlpc.TotalOutboundRequests()
  148. requests := []*QueuedPromRequest{}
  149. rlpc.queue.Each(func(_ int, req *workRequest) {
  150. requests = append(requests, &QueuedPromRequest{
  151. Context: req.contextName,
  152. Query: req.query,
  153. QueueTime: time.Since(req.start).Milliseconds(),
  154. })
  155. })
  156. return &PrometheusQueueState{
  157. QueuedRequests: requests,
  158. OutboundRequests: outbound,
  159. TotalRequests: outbound + len(requests),
  160. MaxQueryConcurrency: env.GetMaxQueryConcurrency(),
  161. }, nil
  162. }
  163. // LogPrometheusClientState logs the current state, with respect to outbound requests, if that
  164. // information is available.
  165. func LogPrometheusClientState(client prometheus.Client) {
  166. if rc, ok := client.(requestCounter); ok {
  167. queued := rc.TotalQueuedRequests()
  168. outbound := rc.TotalOutboundRequests()
  169. total := queued + outbound
  170. log.Infof("Outbound Requests: %d, Queued Requests: %d, Total Requests: %d", outbound, queued, total)
  171. }
  172. }
  173. // GetPrometheusMetrics returns a list of the state of Prometheus metric used by kubecost using the provided client
  174. func GetPrometheusMetrics(client prometheus.Client, offset string) PrometheusDiagnostics {
  175. ctx := NewNamedContext(client, DiagnosticContextName)
  176. var result []*PrometheusDiagnostic
  177. for _, definition := range diagnosticDefinitions {
  178. pd := definition.NewDiagnostic(offset)
  179. err := pd.executePrometheusDiagnosticQuery(ctx)
  180. // log the errror, append to results anyways, and continue
  181. if err != nil {
  182. log.Errorf(err.Error())
  183. }
  184. result = append(result, pd)
  185. }
  186. return result
  187. }
  188. // GetPrometheusMetricsByID returns a list of the state of specific Prometheus metrics by identifier.
  189. func GetPrometheusMetricsByID(ids []string, client prometheus.Client, offset string) PrometheusDiagnostics {
  190. ctx := NewNamedContext(client, DiagnosticContextName)
  191. var result []*PrometheusDiagnostic
  192. for _, id := range ids {
  193. if definition, ok := diagnosticDefinitions[id]; ok {
  194. pd := definition.NewDiagnostic(offset)
  195. err := pd.executePrometheusDiagnosticQuery(ctx)
  196. // log the errror, append to results anyways, and continue
  197. if err != nil {
  198. log.Errorf(err.Error())
  199. }
  200. result = append(result, pd)
  201. } else {
  202. log.Warnf("Failed to find diagnostic definition for id: %s", id)
  203. }
  204. }
  205. return result
  206. }
  207. // PrometheusDiagnostics is a PrometheusDiagnostic container with helper methods.
  208. type PrometheusDiagnostics []*PrometheusDiagnostic
  209. // HasFailure returns true if any of the diagnostic tests didn't pass.
  210. func (pd PrometheusDiagnostics) HasFailure() bool {
  211. for _, p := range pd {
  212. if !p.Passed {
  213. return true
  214. }
  215. }
  216. return false
  217. }
  218. // diagnosticDefinition is a definition of a diagnostic that can be used to create new
  219. // PrometheusDiagnostic instances using the definition's fields.
  220. type diagnosticDefinition struct {
  221. ID string
  222. QueryFmt string
  223. Label string
  224. Description string
  225. DocLink string
  226. }
  227. // NewDiagnostic creates a new PrometheusDiagnostic instance using the provided definition data.
  228. func (pdd *diagnosticDefinition) NewDiagnostic(offset string) *PrometheusDiagnostic {
  229. // FIXME: Any reasonable way to get the total number of replacements required in the query?
  230. // FIXME: All of the other queries require a single offset replace, but CPUThrottle requires two.
  231. var query string
  232. filter := env.GetPromClusterFilter()
  233. if pdd.ID == CPUThrottlingDiagnosticMetricID {
  234. query = fmt.Sprintf(pdd.QueryFmt, filter, offset, filter, offset)
  235. } else {
  236. query = fmt.Sprintf(pdd.QueryFmt, filter, offset)
  237. }
  238. return &PrometheusDiagnostic{
  239. ID: pdd.ID,
  240. Query: query,
  241. Label: pdd.Label,
  242. Description: pdd.Description,
  243. DocLink: pdd.DocLink,
  244. }
  245. }
  246. // PrometheusDiagnostic holds information about a metric and the query to ensure it is functional
  247. type PrometheusDiagnostic struct {
  248. ID string `json:"id"`
  249. Query string `json:"query"`
  250. Label string `json:"label"`
  251. Description string `json:"description"`
  252. DocLink string `json:"docLink"`
  253. Result []*QueryResult `json:"result"`
  254. Passed bool `json:"passed"`
  255. }
  256. // executePrometheusDiagnosticQuery executes a PrometheusDiagnostic query using the given context
  257. func (pd *PrometheusDiagnostic) executePrometheusDiagnosticQuery(ctx *Context) error {
  258. resultCh := ctx.Query(pd.Query)
  259. result, err := resultCh.Await()
  260. if err != nil {
  261. return fmt.Errorf("prometheus diagnostic %s failed with error: %s", pd.ID, err)
  262. }
  263. if result == nil {
  264. result = []*QueryResult{}
  265. }
  266. pd.Result = result
  267. pd.Passed = len(result) == 0
  268. return nil
  269. }