es_cluster_logging.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. /*
  2. Copyright 2015 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package e2e
  14. import (
  15. "encoding/json"
  16. "fmt"
  17. "strconv"
  18. "strings"
  19. "time"
  20. "k8s.io/kubernetes/pkg/api"
  21. "k8s.io/kubernetes/pkg/labels"
  22. "k8s.io/kubernetes/test/e2e/framework"
  23. . "github.com/onsi/ginkgo"
  24. . "github.com/onsi/gomega"
  25. )
  26. var _ = framework.KubeDescribe("Cluster level logging using Elasticsearch [Feature:Elasticsearch]", func() {
  27. f := framework.NewDefaultFramework("es-logging")
  28. BeforeEach(func() {
  29. // TODO: For now assume we are only testing cluster logging with Elasticsearch
  30. // on GCE. Once we are sure that Elasticsearch cluster level logging
  31. // works for other providers we should widen this scope of this test.
  32. framework.SkipUnlessProviderIs("gce")
  33. })
  34. It("should check that logs from pods on all nodes are ingested into Elasticsearch", func() {
  35. ClusterLevelLoggingWithElasticsearch(f)
  36. })
  37. })
  38. const (
  39. k8sAppKey = "k8s-app"
  40. esValue = "elasticsearch-logging"
  41. fluentdValue = "fluentd-logging"
  42. )
  43. func bodyToJSON(body []byte) (map[string]interface{}, error) {
  44. var r map[string]interface{}
  45. if err := json.Unmarshal(body, &r); err != nil {
  46. framework.Logf("Bad JSON: %s", string(body))
  47. return nil, fmt.Errorf("failed to unmarshal Elasticsearch response: %v", err)
  48. }
  49. return r, nil
  50. }
  51. func nodeInNodeList(nodeName string, nodeList *api.NodeList) bool {
  52. for _, node := range nodeList.Items {
  53. if nodeName == node.Name {
  54. return true
  55. }
  56. }
  57. return false
  58. }
  59. // ClusterLevelLoggingWithElasticsearch is an end to end test for cluster level logging.
  60. func ClusterLevelLoggingWithElasticsearch(f *framework.Framework) {
  61. // graceTime is how long to keep retrying requests for status information.
  62. const graceTime = 5 * time.Minute
  63. // ingestionTimeout is how long to keep retrying to wait for all the
  64. // logs to be ingested.
  65. const ingestionTimeout = 10 * time.Minute
  66. // Check for the existence of the Elasticsearch service.
  67. By("Checking the Elasticsearch service exists.")
  68. s := f.Client.Services(api.NamespaceSystem)
  69. // Make a few attempts to connect. This makes the test robust against
  70. // being run as the first e2e test just after the e2e cluster has been created.
  71. var err error
  72. for start := time.Now(); time.Since(start) < graceTime; time.Sleep(5 * time.Second) {
  73. if _, err = s.Get("elasticsearch-logging"); err == nil {
  74. break
  75. }
  76. framework.Logf("Attempt to check for the existence of the Elasticsearch service failed after %v", time.Since(start))
  77. }
  78. Expect(err).NotTo(HaveOccurred())
  79. // Wait for the Elasticsearch pods to enter the running state.
  80. By("Checking to make sure the Elasticsearch pods are running")
  81. label := labels.SelectorFromSet(labels.Set(map[string]string{k8sAppKey: esValue}))
  82. options := api.ListOptions{LabelSelector: label}
  83. pods, err := f.Client.Pods(api.NamespaceSystem).List(options)
  84. Expect(err).NotTo(HaveOccurred())
  85. for _, pod := range pods.Items {
  86. err = framework.WaitForPodRunningInNamespace(f.Client, &pod)
  87. Expect(err).NotTo(HaveOccurred())
  88. }
  89. By("Checking to make sure we are talking to an Elasticsearch service.")
  90. // Perform a few checks to make sure this looks like an Elasticsearch cluster.
  91. var statusCode float64
  92. var esResponse map[string]interface{}
  93. err = nil
  94. var body []byte
  95. for start := time.Now(); time.Since(start) < graceTime; time.Sleep(10 * time.Second) {
  96. proxyRequest, errProxy := framework.GetServicesProxyRequest(f.Client, f.Client.Get())
  97. if errProxy != nil {
  98. framework.Logf("After %v failed to get services proxy request: %v", time.Since(start), errProxy)
  99. continue
  100. }
  101. // Query against the root URL for Elasticsearch.
  102. body, err = proxyRequest.Namespace(api.NamespaceSystem).
  103. Name("elasticsearch-logging").
  104. DoRaw()
  105. if err != nil {
  106. framework.Logf("After %v proxy call to elasticsearch-loigging failed: %v", time.Since(start), err)
  107. continue
  108. }
  109. esResponse, err = bodyToJSON(body)
  110. if err != nil {
  111. framework.Logf("After %v failed to convert Elasticsearch JSON response %v to map[string]interface{}: %v", time.Since(start), string(body), err)
  112. continue
  113. }
  114. statusIntf, ok := esResponse["status"]
  115. if !ok {
  116. framework.Logf("After %v Elasticsearch response has no status field: %v", time.Since(start), esResponse)
  117. continue
  118. }
  119. statusCode, ok = statusIntf.(float64)
  120. if !ok {
  121. // Assume this is a string returning Failure. Retry.
  122. framework.Logf("After %v expected status to be a float64 but got %v of type %T", time.Since(start), statusIntf, statusIntf)
  123. continue
  124. }
  125. if int(statusCode) != 200 {
  126. framework.Logf("After %v Elasticsearch cluster has a bad status: %v", time.Since(start), statusCode)
  127. continue
  128. }
  129. break
  130. }
  131. Expect(err).NotTo(HaveOccurred())
  132. if int(statusCode) != 200 {
  133. framework.Failf("Elasticsearch cluster has a bad status: %v", statusCode)
  134. }
  135. // Check to see if have a cluster_name field.
  136. clusterName, ok := esResponse["cluster_name"]
  137. if !ok {
  138. framework.Failf("No cluster_name field in Elasticsearch response: %v", esResponse)
  139. }
  140. if clusterName != "kubernetes-logging" {
  141. framework.Failf("Connected to wrong cluster %q (expecting kubernetes_logging)", clusterName)
  142. }
  143. // Now assume we really are talking to an Elasticsearch instance.
  144. // Check the cluster health.
  145. By("Checking health of Elasticsearch service.")
  146. healthy := false
  147. for start := time.Now(); time.Since(start) < graceTime; time.Sleep(5 * time.Second) {
  148. proxyRequest, errProxy := framework.GetServicesProxyRequest(f.Client, f.Client.Get())
  149. if errProxy != nil {
  150. framework.Logf("After %v failed to get services proxy request: %v", time.Since(start), errProxy)
  151. continue
  152. }
  153. body, err = proxyRequest.Namespace(api.NamespaceSystem).
  154. Name("elasticsearch-logging").
  155. Suffix("_cluster/health").
  156. Param("level", "indices").
  157. DoRaw()
  158. if err != nil {
  159. continue
  160. }
  161. health, err := bodyToJSON(body)
  162. if err != nil {
  163. framework.Logf("Bad json response from elasticsearch: %v", err)
  164. continue
  165. }
  166. statusIntf, ok := health["status"]
  167. if !ok {
  168. framework.Logf("No status field found in cluster health response: %v", health)
  169. continue
  170. }
  171. status := statusIntf.(string)
  172. if status != "green" && status != "yellow" {
  173. framework.Logf("Cluster health has bad status: %v", health)
  174. continue
  175. }
  176. if err == nil && ok {
  177. healthy = true
  178. break
  179. }
  180. }
  181. if !healthy {
  182. framework.Failf("After %v elasticsearch cluster is not healthy", graceTime)
  183. }
  184. // Obtain a list of nodes so we can place one synthetic logger on each node.
  185. nodes := framework.GetReadySchedulableNodesOrDie(f.Client)
  186. nodeCount := len(nodes.Items)
  187. if nodeCount == 0 {
  188. framework.Failf("Failed to find any nodes")
  189. }
  190. framework.Logf("Found %d nodes.", len(nodes.Items))
  191. // Filter out unhealthy nodes.
  192. // Previous tests may have cause failures of some nodes. Let's skip
  193. // 'Not Ready' nodes, just in case (there is no need to fail the test).
  194. framework.FilterNodes(nodes, func(node api.Node) bool {
  195. return framework.IsNodeConditionSetAsExpected(&node, api.NodeReady, true)
  196. })
  197. if len(nodes.Items) < 2 {
  198. framework.Failf("Less than two nodes were found Ready: %d", len(nodes.Items))
  199. }
  200. framework.Logf("Found %d healthy nodes.", len(nodes.Items))
  201. // Wait for the Fluentd pods to enter the running state.
  202. By("Checking to make sure the Fluentd pod are running on each healthy node")
  203. label = labels.SelectorFromSet(labels.Set(map[string]string{k8sAppKey: fluentdValue}))
  204. options = api.ListOptions{LabelSelector: label}
  205. fluentdPods, err := f.Client.Pods(api.NamespaceSystem).List(options)
  206. Expect(err).NotTo(HaveOccurred())
  207. for _, pod := range fluentdPods.Items {
  208. if nodeInNodeList(pod.Spec.NodeName, nodes) {
  209. err = framework.WaitForPodRunningInNamespace(f.Client, &pod)
  210. Expect(err).NotTo(HaveOccurred())
  211. }
  212. }
  213. // Check if each healthy node has fluentd running on it
  214. for _, node := range nodes.Items {
  215. exists := false
  216. for _, pod := range fluentdPods.Items {
  217. if pod.Spec.NodeName == node.Name {
  218. exists = true
  219. break
  220. }
  221. }
  222. if !exists {
  223. framework.Failf("Node %v does not have fluentd pod running on it.", node.Name)
  224. }
  225. }
  226. // Create a unique root name for the resources in this test to permit
  227. // parallel executions of this test.
  228. // Use a unique namespace for the resources created in this test.
  229. ns := f.Namespace.Name
  230. name := "synthlogger"
  231. // Form a unique name to taint log lines to be collected.
  232. // Replace '-' characters with '_' to prevent the analyzer from breaking apart names.
  233. taintName := strings.Replace(ns+name, "-", "_", -1)
  234. framework.Logf("Tainting log lines with %v", taintName)
  235. // podNames records the names of the synthetic logging pods that are created in the
  236. // loop below.
  237. var podNames []string
  238. // countTo is the number of log lines emitted (and checked) for each synthetic logging pod.
  239. const countTo = 100
  240. // Instantiate a synthetic logger pod on each node.
  241. for i, node := range nodes.Items {
  242. podName := fmt.Sprintf("%s-%d", name, i)
  243. _, err := f.Client.Pods(ns).Create(&api.Pod{
  244. ObjectMeta: api.ObjectMeta{
  245. Name: podName,
  246. Labels: map[string]string{"name": name},
  247. },
  248. Spec: api.PodSpec{
  249. Containers: []api.Container{
  250. {
  251. Name: "synth-logger",
  252. Image: "gcr.io/google_containers/ubuntu:14.04",
  253. // notice: the subshell syntax is escaped with `$$`
  254. Command: []string{"bash", "-c", fmt.Sprintf("i=0; while ((i < %d)); do echo \"%d %s $i %s\"; i=$$(($i+1)); done", countTo, i, taintName, podName)},
  255. },
  256. },
  257. NodeName: node.Name,
  258. RestartPolicy: api.RestartPolicyNever,
  259. },
  260. })
  261. Expect(err).NotTo(HaveOccurred())
  262. podNames = append(podNames, podName)
  263. }
  264. // Cleanup the pods when we are done.
  265. defer func() {
  266. for _, pod := range podNames {
  267. if err = f.Client.Pods(ns).Delete(pod, nil); err != nil {
  268. framework.Logf("Failed to delete pod %s: %v", pod, err)
  269. }
  270. }
  271. }()
  272. // Wait for the synthetic logging pods to finish.
  273. By("Waiting for the pods to succeed.")
  274. for _, pod := range podNames {
  275. err = framework.WaitForPodSuccessInNamespace(f.Client, pod, "synth-logger", ns)
  276. Expect(err).NotTo(HaveOccurred())
  277. }
  278. // Wait a bit for the log information to make it into Elasticsearch.
  279. time.Sleep(30 * time.Second)
  280. // Make several attempts to observe the logs ingested into Elasticsearch.
  281. By("Checking all the log lines were ingested into Elasticsearch")
  282. totalMissing := 0
  283. expected := nodeCount * countTo
  284. missingPerNode := []int{}
  285. for start := time.Now(); time.Since(start) < ingestionTimeout; time.Sleep(25 * time.Second) {
  286. // Debugging code to report the status of the elasticsearch logging endpoints.
  287. selector := labels.Set{k8sAppKey: esValue}.AsSelector()
  288. options := api.ListOptions{LabelSelector: selector}
  289. esPods, err := f.Client.Pods(api.NamespaceSystem).List(options)
  290. if err != nil {
  291. framework.Logf("Attempt to list Elasticsearch nodes encountered a problem -- may retry: %v", err)
  292. continue
  293. } else {
  294. for i, pod := range esPods.Items {
  295. framework.Logf("pod %d: %s PodIP %s phase %s condition %+v", i, pod.Name, pod.Status.PodIP, pod.Status.Phase,
  296. pod.Status.Conditions)
  297. }
  298. }
  299. proxyRequest, errProxy := framework.GetServicesProxyRequest(f.Client, f.Client.Get())
  300. if errProxy != nil {
  301. framework.Logf("After %v failed to get services proxy request: %v", time.Since(start), errProxy)
  302. continue
  303. }
  304. // Ask Elasticsearch to return all the log lines that were tagged with the underscore
  305. // version of the name. Ask for twice as many log lines as we expect to check for
  306. // duplication bugs.
  307. body, err = proxyRequest.Namespace(api.NamespaceSystem).
  308. Name("elasticsearch-logging").
  309. Suffix("_search").
  310. Param("q", fmt.Sprintf("log:%s", taintName)).
  311. Param("size", strconv.Itoa(2*expected)).
  312. DoRaw()
  313. if err != nil {
  314. framework.Logf("After %v failed to make proxy call to elasticsearch-logging: %v", time.Since(start), err)
  315. continue
  316. }
  317. response, err := bodyToJSON(body)
  318. if err != nil {
  319. framework.Logf("After %v failed to unmarshal response: %v", time.Since(start), err)
  320. framework.Logf("Body: %s", string(body))
  321. continue
  322. }
  323. hits, ok := response["hits"].(map[string]interface{})
  324. if !ok {
  325. framework.Logf("response[hits] not of the expected type: %T", response["hits"])
  326. continue
  327. }
  328. totalF, ok := hits["total"].(float64)
  329. if !ok {
  330. framework.Logf("After %v hits[total] not of the expected type: %T", time.Since(start), hits["total"])
  331. continue
  332. }
  333. total := int(totalF)
  334. if total != expected {
  335. framework.Logf("After %v expecting to find %d log lines but saw %d", time.Since(start), expected, total)
  336. }
  337. h, ok := hits["hits"].([]interface{})
  338. if !ok {
  339. framework.Logf("After %v hits not of the expected type: %T", time.Since(start), hits["hits"])
  340. continue
  341. }
  342. // Initialize data-structure for observing counts.
  343. observed := make([][]int, nodeCount)
  344. for i := range observed {
  345. observed[i] = make([]int, countTo)
  346. }
  347. // Iterate over the hits and populate the observed array.
  348. for _, e := range h {
  349. l, ok := e.(map[string]interface{})
  350. if !ok {
  351. framework.Logf("element of hit not of expected type: %T", e)
  352. continue
  353. }
  354. source, ok := l["_source"].(map[string]interface{})
  355. if !ok {
  356. framework.Logf("_source not of the expected type: %T", l["_source"])
  357. continue
  358. }
  359. msg, ok := source["log"].(string)
  360. if !ok {
  361. framework.Logf("log not of the expected type: %T", source["log"])
  362. continue
  363. }
  364. words := strings.Split(msg, " ")
  365. if len(words) != 4 {
  366. framework.Logf("Malformed log line: %s", msg)
  367. continue
  368. }
  369. n, err := strconv.ParseUint(words[0], 10, 0)
  370. if err != nil {
  371. framework.Logf("Expecting numer of node as first field of %s", msg)
  372. continue
  373. }
  374. if n < 0 || int(n) >= nodeCount {
  375. framework.Logf("Node count index out of range: %d", nodeCount)
  376. continue
  377. }
  378. index, err := strconv.ParseUint(words[2], 10, 0)
  379. if err != nil {
  380. framework.Logf("Expecting number as third field of %s", msg)
  381. continue
  382. }
  383. if index < 0 || index >= countTo {
  384. framework.Logf("Index value out of range: %d", index)
  385. continue
  386. }
  387. if words[1] != taintName {
  388. framework.Logf("Elasticsearch query return unexpected log line: %s", msg)
  389. continue
  390. }
  391. // Record the observation of a log line from node n at the given index.
  392. observed[n][index]++
  393. }
  394. // Make sure we correctly observed the expected log lines from each node.
  395. totalMissing = 0
  396. missingPerNode = make([]int, nodeCount)
  397. incorrectCount := false
  398. for n := range observed {
  399. for i, c := range observed[n] {
  400. if c == 0 {
  401. totalMissing++
  402. missingPerNode[n]++
  403. }
  404. if c < 0 || c > 1 {
  405. framework.Logf("Got incorrect count for node %d index %d: %d", n, i, c)
  406. incorrectCount = true
  407. }
  408. }
  409. }
  410. if incorrectCount {
  411. framework.Logf("After %v es still return duplicated log lines", time.Since(start))
  412. continue
  413. }
  414. if totalMissing != 0 {
  415. framework.Logf("After %v still missing %d log lines", time.Since(start), totalMissing)
  416. continue
  417. }
  418. framework.Logf("After %s found all %d log lines", time.Since(start), expected)
  419. return
  420. }
  421. for n := range missingPerNode {
  422. if missingPerNode[n] > 0 {
  423. framework.Logf("Node %d %s is missing %d logs", n, nodes.Items[n].Name, missingPerNode[n])
  424. opts := &api.PodLogOptions{}
  425. body, err = f.Client.Pods(ns).GetLogs(podNames[n], opts).DoRaw()
  426. if err != nil {
  427. framework.Logf("Cannot get logs from pod %v", podNames[n])
  428. continue
  429. }
  430. framework.Logf("Pod %s has the following logs: %s", podNames[n], body)
  431. for _, pod := range fluentdPods.Items {
  432. if pod.Spec.NodeName == nodes.Items[n].Name {
  433. body, err = f.Client.Pods(api.NamespaceSystem).GetLogs(pod.Name, opts).DoRaw()
  434. if err != nil {
  435. framework.Logf("Cannot get logs from pod %v", pod.Name)
  436. break
  437. }
  438. framework.Logf("Fluentd Pod %s on node %s has the following logs: %s", pod.Name, nodes.Items[n].Name, body)
  439. break
  440. }
  441. }
  442. }
  443. }
  444. framework.Failf("Failed to find all %d log lines", expected)
  445. }