paasta-tools 1.21.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. k8s_itests/__init__.py +0 -0
  2. k8s_itests/test_autoscaling.py +23 -0
  3. k8s_itests/utils.py +38 -0
  4. paasta_tools/__init__.py +20 -0
  5. paasta_tools/adhoc_tools.py +142 -0
  6. paasta_tools/api/__init__.py +13 -0
  7. paasta_tools/api/api.py +330 -0
  8. paasta_tools/api/api_docs/swagger.json +2323 -0
  9. paasta_tools/api/client.py +106 -0
  10. paasta_tools/api/settings.py +33 -0
  11. paasta_tools/api/tweens/__init__.py +6 -0
  12. paasta_tools/api/tweens/auth.py +125 -0
  13. paasta_tools/api/tweens/profiling.py +108 -0
  14. paasta_tools/api/tweens/request_logger.py +124 -0
  15. paasta_tools/api/views/__init__.py +13 -0
  16. paasta_tools/api/views/autoscaler.py +100 -0
  17. paasta_tools/api/views/exception.py +45 -0
  18. paasta_tools/api/views/flink.py +73 -0
  19. paasta_tools/api/views/instance.py +395 -0
  20. paasta_tools/api/views/pause_autoscaler.py +71 -0
  21. paasta_tools/api/views/remote_run.py +113 -0
  22. paasta_tools/api/views/resources.py +76 -0
  23. paasta_tools/api/views/service.py +35 -0
  24. paasta_tools/api/views/version.py +25 -0
  25. paasta_tools/apply_external_resources.py +79 -0
  26. paasta_tools/async_utils.py +109 -0
  27. paasta_tools/autoscaling/__init__.py +0 -0
  28. paasta_tools/autoscaling/autoscaling_service_lib.py +57 -0
  29. paasta_tools/autoscaling/forecasting.py +106 -0
  30. paasta_tools/autoscaling/max_all_k8s_services.py +41 -0
  31. paasta_tools/autoscaling/pause_service_autoscaler.py +77 -0
  32. paasta_tools/autoscaling/utils.py +52 -0
  33. paasta_tools/bounce_lib.py +184 -0
  34. paasta_tools/broadcast_log_to_services.py +62 -0
  35. paasta_tools/cassandracluster_tools.py +210 -0
  36. paasta_tools/check_autoscaler_max_instances.py +212 -0
  37. paasta_tools/check_cassandracluster_services_replication.py +35 -0
  38. paasta_tools/check_flink_services_health.py +203 -0
  39. paasta_tools/check_kubernetes_api.py +57 -0
  40. paasta_tools/check_kubernetes_services_replication.py +141 -0
  41. paasta_tools/check_oom_events.py +244 -0
  42. paasta_tools/check_services_replication_tools.py +324 -0
  43. paasta_tools/check_spark_jobs.py +234 -0
  44. paasta_tools/cleanup_kubernetes_cr.py +138 -0
  45. paasta_tools/cleanup_kubernetes_crd.py +145 -0
  46. paasta_tools/cleanup_kubernetes_jobs.py +344 -0
  47. paasta_tools/cleanup_tron_namespaces.py +96 -0
  48. paasta_tools/cli/__init__.py +13 -0
  49. paasta_tools/cli/authentication.py +85 -0
  50. paasta_tools/cli/cli.py +260 -0
  51. paasta_tools/cli/cmds/__init__.py +13 -0
  52. paasta_tools/cli/cmds/autoscale.py +143 -0
  53. paasta_tools/cli/cmds/check.py +334 -0
  54. paasta_tools/cli/cmds/cook_image.py +147 -0
  55. paasta_tools/cli/cmds/get_docker_image.py +76 -0
  56. paasta_tools/cli/cmds/get_image_version.py +172 -0
  57. paasta_tools/cli/cmds/get_latest_deployment.py +93 -0
  58. paasta_tools/cli/cmds/info.py +155 -0
  59. paasta_tools/cli/cmds/itest.py +117 -0
  60. paasta_tools/cli/cmds/list.py +66 -0
  61. paasta_tools/cli/cmds/list_clusters.py +42 -0
  62. paasta_tools/cli/cmds/list_deploy_queue.py +171 -0
  63. paasta_tools/cli/cmds/list_namespaces.py +84 -0
  64. paasta_tools/cli/cmds/local_run.py +1396 -0
  65. paasta_tools/cli/cmds/logs.py +1601 -0
  66. paasta_tools/cli/cmds/mark_for_deployment.py +1988 -0
  67. paasta_tools/cli/cmds/mesh_status.py +174 -0
  68. paasta_tools/cli/cmds/pause_service_autoscaler.py +107 -0
  69. paasta_tools/cli/cmds/push_to_registry.py +275 -0
  70. paasta_tools/cli/cmds/remote_run.py +252 -0
  71. paasta_tools/cli/cmds/rollback.py +347 -0
  72. paasta_tools/cli/cmds/secret.py +549 -0
  73. paasta_tools/cli/cmds/security_check.py +59 -0
  74. paasta_tools/cli/cmds/spark_run.py +1400 -0
  75. paasta_tools/cli/cmds/start_stop_restart.py +401 -0
  76. paasta_tools/cli/cmds/status.py +2302 -0
  77. paasta_tools/cli/cmds/validate.py +1012 -0
  78. paasta_tools/cli/cmds/wait_for_deployment.py +275 -0
  79. paasta_tools/cli/fsm/__init__.py +13 -0
  80. paasta_tools/cli/fsm/autosuggest.py +82 -0
  81. paasta_tools/cli/fsm/template/README.md +8 -0
  82. paasta_tools/cli/fsm/template/cookiecutter.json +7 -0
  83. paasta_tools/cli/fsm/template/{{cookiecutter.service}}/kubernetes-PROD.yaml +91 -0
  84. paasta_tools/cli/fsm/template/{{cookiecutter.service}}/monitoring.yaml +20 -0
  85. paasta_tools/cli/fsm/template/{{cookiecutter.service}}/service.yaml +8 -0
  86. paasta_tools/cli/fsm/template/{{cookiecutter.service}}/smartstack.yaml +6 -0
  87. paasta_tools/cli/fsm_cmd.py +121 -0
  88. paasta_tools/cli/paasta_tabcomplete.sh +23 -0
  89. paasta_tools/cli/schemas/adhoc_schema.json +199 -0
  90. paasta_tools/cli/schemas/autoscaling_schema.json +91 -0
  91. paasta_tools/cli/schemas/autotuned_defaults/cassandracluster_schema.json +37 -0
  92. paasta_tools/cli/schemas/autotuned_defaults/kubernetes_schema.json +89 -0
  93. paasta_tools/cli/schemas/deploy_schema.json +173 -0
  94. paasta_tools/cli/schemas/eks_schema.json +970 -0
  95. paasta_tools/cli/schemas/kubernetes_schema.json +970 -0
  96. paasta_tools/cli/schemas/rollback_schema.json +160 -0
  97. paasta_tools/cli/schemas/service_schema.json +25 -0
  98. paasta_tools/cli/schemas/smartstack_schema.json +322 -0
  99. paasta_tools/cli/schemas/tron_schema.json +699 -0
  100. paasta_tools/cli/utils.py +1118 -0
  101. paasta_tools/clusterman.py +21 -0
  102. paasta_tools/config_utils.py +385 -0
  103. paasta_tools/contrib/__init__.py +0 -0
  104. paasta_tools/contrib/bounce_log_latency_parser.py +68 -0
  105. paasta_tools/contrib/check_manual_oapi_changes.sh +24 -0
  106. paasta_tools/contrib/check_orphans.py +306 -0
  107. paasta_tools/contrib/create_dynamodb_table.py +35 -0
  108. paasta_tools/contrib/create_paasta_playground.py +105 -0
  109. paasta_tools/contrib/emit_allocated_cpu_metrics.py +50 -0
  110. paasta_tools/contrib/get_running_task_allocation.py +346 -0
  111. paasta_tools/contrib/habitat_fixer.py +86 -0
  112. paasta_tools/contrib/ide_helper.py +316 -0
  113. paasta_tools/contrib/is_pod_healthy_in_proxy.py +139 -0
  114. paasta_tools/contrib/is_pod_healthy_in_smartstack.py +50 -0
  115. paasta_tools/contrib/kill_bad_containers.py +109 -0
  116. paasta_tools/contrib/mass-deploy-tag.sh +44 -0
  117. paasta_tools/contrib/mock_patch_checker.py +86 -0
  118. paasta_tools/contrib/paasta_update_soa_memcpu.py +520 -0
  119. paasta_tools/contrib/render_template.py +129 -0
  120. paasta_tools/contrib/rightsizer_soaconfigs_update.py +348 -0
  121. paasta_tools/contrib/service_shard_remove.py +157 -0
  122. paasta_tools/contrib/service_shard_update.py +373 -0
  123. paasta_tools/contrib/shared_ip_check.py +77 -0
  124. paasta_tools/contrib/timeouts_metrics_prom.py +64 -0
  125. paasta_tools/delete_kubernetes_deployments.py +89 -0
  126. paasta_tools/deployment_utils.py +44 -0
  127. paasta_tools/docker_wrapper.py +234 -0
  128. paasta_tools/docker_wrapper_imports.py +13 -0
  129. paasta_tools/drain_lib.py +351 -0
  130. paasta_tools/dump_locally_running_services.py +71 -0
  131. paasta_tools/eks_tools.py +119 -0
  132. paasta_tools/envoy_tools.py +373 -0
  133. paasta_tools/firewall.py +504 -0
  134. paasta_tools/firewall_logging.py +154 -0
  135. paasta_tools/firewall_update.py +172 -0
  136. paasta_tools/flink_tools.py +345 -0
  137. paasta_tools/flinkeks_tools.py +90 -0
  138. paasta_tools/frameworks/__init__.py +0 -0
  139. paasta_tools/frameworks/adhoc_scheduler.py +71 -0
  140. paasta_tools/frameworks/constraints.py +87 -0
  141. paasta_tools/frameworks/native_scheduler.py +652 -0
  142. paasta_tools/frameworks/native_service_config.py +301 -0
  143. paasta_tools/frameworks/task_store.py +245 -0
  144. paasta_tools/generate_all_deployments +9 -0
  145. paasta_tools/generate_authenticating_services.py +94 -0
  146. paasta_tools/generate_deployments_for_service.py +255 -0
  147. paasta_tools/generate_services_file.py +114 -0
  148. paasta_tools/generate_services_yaml.py +30 -0
  149. paasta_tools/hacheck.py +76 -0
  150. paasta_tools/instance/__init__.py +0 -0
  151. paasta_tools/instance/hpa_metrics_parser.py +122 -0
  152. paasta_tools/instance/kubernetes.py +1362 -0
  153. paasta_tools/iptables.py +240 -0
  154. paasta_tools/kafkacluster_tools.py +143 -0
  155. paasta_tools/kubernetes/__init__.py +0 -0
  156. paasta_tools/kubernetes/application/__init__.py +0 -0
  157. paasta_tools/kubernetes/application/controller_wrappers.py +476 -0
  158. paasta_tools/kubernetes/application/tools.py +90 -0
  159. paasta_tools/kubernetes/bin/__init__.py +0 -0
  160. paasta_tools/kubernetes/bin/kubernetes_remove_evicted_pods.py +164 -0
  161. paasta_tools/kubernetes/bin/paasta_cleanup_remote_run_resources.py +135 -0
  162. paasta_tools/kubernetes/bin/paasta_cleanup_stale_nodes.py +181 -0
  163. paasta_tools/kubernetes/bin/paasta_secrets_sync.py +758 -0
  164. paasta_tools/kubernetes/remote_run.py +558 -0
  165. paasta_tools/kubernetes_tools.py +4679 -0
  166. paasta_tools/list_kubernetes_service_instances.py +128 -0
  167. paasta_tools/list_tron_namespaces.py +60 -0
  168. paasta_tools/long_running_service_tools.py +678 -0
  169. paasta_tools/mac_address.py +44 -0
  170. paasta_tools/marathon_dashboard.py +0 -0
  171. paasta_tools/mesos/__init__.py +0 -0
  172. paasta_tools/mesos/cfg.py +46 -0
  173. paasta_tools/mesos/cluster.py +60 -0
  174. paasta_tools/mesos/exceptions.py +59 -0
  175. paasta_tools/mesos/framework.py +77 -0
  176. paasta_tools/mesos/log.py +48 -0
  177. paasta_tools/mesos/master.py +306 -0
  178. paasta_tools/mesos/mesos_file.py +169 -0
  179. paasta_tools/mesos/parallel.py +52 -0
  180. paasta_tools/mesos/slave.py +115 -0
  181. paasta_tools/mesos/task.py +94 -0
  182. paasta_tools/mesos/util.py +69 -0
  183. paasta_tools/mesos/zookeeper.py +37 -0
  184. paasta_tools/mesos_maintenance.py +848 -0
  185. paasta_tools/mesos_tools.py +1051 -0
  186. paasta_tools/metrics/__init__.py +0 -0
  187. paasta_tools/metrics/metastatus_lib.py +1110 -0
  188. paasta_tools/metrics/metrics_lib.py +217 -0
  189. paasta_tools/monitoring/__init__.py +13 -0
  190. paasta_tools/monitoring/check_k8s_api_performance.py +110 -0
  191. paasta_tools/monitoring_tools.py +652 -0
  192. paasta_tools/monkrelaycluster_tools.py +146 -0
  193. paasta_tools/nrtsearchservice_tools.py +143 -0
  194. paasta_tools/nrtsearchserviceeks_tools.py +68 -0
  195. paasta_tools/oom_logger.py +321 -0
  196. paasta_tools/paasta_deploy_tron_jobs +3 -0
  197. paasta_tools/paasta_execute_docker_command.py +123 -0
  198. paasta_tools/paasta_native_serviceinit.py +21 -0
  199. paasta_tools/paasta_service_config_loader.py +201 -0
  200. paasta_tools/paastaapi/__init__.py +29 -0
  201. paasta_tools/paastaapi/api/__init__.py +3 -0
  202. paasta_tools/paastaapi/api/autoscaler_api.py +302 -0
  203. paasta_tools/paastaapi/api/default_api.py +569 -0
  204. paasta_tools/paastaapi/api/remote_run_api.py +604 -0
  205. paasta_tools/paastaapi/api/resources_api.py +157 -0
  206. paasta_tools/paastaapi/api/service_api.py +1736 -0
  207. paasta_tools/paastaapi/api_client.py +818 -0
  208. paasta_tools/paastaapi/apis/__init__.py +22 -0
  209. paasta_tools/paastaapi/configuration.py +455 -0
  210. paasta_tools/paastaapi/exceptions.py +137 -0
  211. paasta_tools/paastaapi/model/__init__.py +5 -0
  212. paasta_tools/paastaapi/model/adhoc_launch_history.py +176 -0
  213. paasta_tools/paastaapi/model/autoscaler_count_msg.py +176 -0
  214. paasta_tools/paastaapi/model/deploy_queue.py +178 -0
  215. paasta_tools/paastaapi/model/deploy_queue_service_instance.py +194 -0
  216. paasta_tools/paastaapi/model/envoy_backend.py +185 -0
  217. paasta_tools/paastaapi/model/envoy_location.py +184 -0
  218. paasta_tools/paastaapi/model/envoy_status.py +181 -0
  219. paasta_tools/paastaapi/model/flink_cluster_overview.py +188 -0
  220. paasta_tools/paastaapi/model/flink_config.py +173 -0
  221. paasta_tools/paastaapi/model/flink_job.py +186 -0
  222. paasta_tools/paastaapi/model/flink_job_details.py +192 -0
  223. paasta_tools/paastaapi/model/flink_jobs.py +175 -0
  224. paasta_tools/paastaapi/model/float_and_error.py +173 -0
  225. paasta_tools/paastaapi/model/hpa_metric.py +176 -0
  226. paasta_tools/paastaapi/model/inline_object.py +170 -0
  227. paasta_tools/paastaapi/model/inline_response200.py +170 -0
  228. paasta_tools/paastaapi/model/inline_response2001.py +170 -0
  229. paasta_tools/paastaapi/model/instance_bounce_status.py +200 -0
  230. paasta_tools/paastaapi/model/instance_mesh_status.py +186 -0
  231. paasta_tools/paastaapi/model/instance_status.py +220 -0
  232. paasta_tools/paastaapi/model/instance_status_adhoc.py +187 -0
  233. paasta_tools/paastaapi/model/instance_status_cassandracluster.py +173 -0
  234. paasta_tools/paastaapi/model/instance_status_flink.py +173 -0
  235. paasta_tools/paastaapi/model/instance_status_kafkacluster.py +173 -0
  236. paasta_tools/paastaapi/model/instance_status_kubernetes.py +263 -0
  237. paasta_tools/paastaapi/model/instance_status_kubernetes_autoscaling_status.py +187 -0
  238. paasta_tools/paastaapi/model/instance_status_kubernetes_v2.py +197 -0
  239. paasta_tools/paastaapi/model/instance_status_tron.py +204 -0
  240. paasta_tools/paastaapi/model/instance_tasks.py +182 -0
  241. paasta_tools/paastaapi/model/integer_and_error.py +173 -0
  242. paasta_tools/paastaapi/model/kubernetes_container.py +178 -0
  243. paasta_tools/paastaapi/model/kubernetes_container_v2.py +219 -0
  244. paasta_tools/paastaapi/model/kubernetes_healthcheck.py +176 -0
  245. paasta_tools/paastaapi/model/kubernetes_pod.py +201 -0
  246. paasta_tools/paastaapi/model/kubernetes_pod_event.py +176 -0
  247. paasta_tools/paastaapi/model/kubernetes_pod_v2.py +213 -0
  248. paasta_tools/paastaapi/model/kubernetes_replica_set.py +185 -0
  249. paasta_tools/paastaapi/model/kubernetes_version.py +202 -0
  250. paasta_tools/paastaapi/model/remote_run_outcome.py +189 -0
  251. paasta_tools/paastaapi/model/remote_run_start.py +185 -0
  252. paasta_tools/paastaapi/model/remote_run_stop.py +176 -0
  253. paasta_tools/paastaapi/model/remote_run_token.py +173 -0
  254. paasta_tools/paastaapi/model/resource.py +187 -0
  255. paasta_tools/paastaapi/model/resource_item.py +187 -0
  256. paasta_tools/paastaapi/model/resource_value.py +176 -0
  257. paasta_tools/paastaapi/model/smartstack_backend.py +191 -0
  258. paasta_tools/paastaapi/model/smartstack_location.py +181 -0
  259. paasta_tools/paastaapi/model/smartstack_status.py +181 -0
  260. paasta_tools/paastaapi/model/task_tail_lines.py +176 -0
  261. paasta_tools/paastaapi/model_utils.py +1879 -0
  262. paasta_tools/paastaapi/models/__init__.py +62 -0
  263. paasta_tools/paastaapi/rest.py +287 -0
  264. paasta_tools/prune_completed_pods.py +220 -0
  265. paasta_tools/puppet_service_tools.py +59 -0
  266. paasta_tools/py.typed +1 -0
  267. paasta_tools/remote_git.py +127 -0
  268. paasta_tools/run-paasta-api-in-dev-mode.py +57 -0
  269. paasta_tools/run-paasta-api-playground.py +51 -0
  270. paasta_tools/secret_providers/__init__.py +66 -0
  271. paasta_tools/secret_providers/vault.py +214 -0
  272. paasta_tools/secret_tools.py +277 -0
  273. paasta_tools/setup_istio_mesh.py +353 -0
  274. paasta_tools/setup_kubernetes_cr.py +412 -0
  275. paasta_tools/setup_kubernetes_crd.py +138 -0
  276. paasta_tools/setup_kubernetes_internal_crd.py +154 -0
  277. paasta_tools/setup_kubernetes_job.py +353 -0
  278. paasta_tools/setup_prometheus_adapter_config.py +1028 -0
  279. paasta_tools/setup_tron_namespace.py +248 -0
  280. paasta_tools/slack.py +75 -0
  281. paasta_tools/smartstack_tools.py +676 -0
  282. paasta_tools/spark_tools.py +283 -0
  283. paasta_tools/synapse_srv_namespaces_fact.py +42 -0
  284. paasta_tools/tron/__init__.py +0 -0
  285. paasta_tools/tron/client.py +158 -0
  286. paasta_tools/tron/tron_command_context.py +194 -0
  287. paasta_tools/tron/tron_timeutils.py +101 -0
  288. paasta_tools/tron_tools.py +1448 -0
  289. paasta_tools/utils.py +4307 -0
  290. paasta_tools/yaml_tools.py +44 -0
  291. paasta_tools-1.21.3.data/scripts/apply_external_resources.py +79 -0
  292. paasta_tools-1.21.3.data/scripts/bounce_log_latency_parser.py +68 -0
  293. paasta_tools-1.21.3.data/scripts/check_autoscaler_max_instances.py +212 -0
  294. paasta_tools-1.21.3.data/scripts/check_cassandracluster_services_replication.py +35 -0
  295. paasta_tools-1.21.3.data/scripts/check_flink_services_health.py +203 -0
  296. paasta_tools-1.21.3.data/scripts/check_kubernetes_api.py +57 -0
  297. paasta_tools-1.21.3.data/scripts/check_kubernetes_services_replication.py +141 -0
  298. paasta_tools-1.21.3.data/scripts/check_manual_oapi_changes.sh +24 -0
  299. paasta_tools-1.21.3.data/scripts/check_oom_events.py +244 -0
  300. paasta_tools-1.21.3.data/scripts/check_orphans.py +306 -0
  301. paasta_tools-1.21.3.data/scripts/check_spark_jobs.py +234 -0
  302. paasta_tools-1.21.3.data/scripts/cleanup_kubernetes_cr.py +138 -0
  303. paasta_tools-1.21.3.data/scripts/cleanup_kubernetes_crd.py +145 -0
  304. paasta_tools-1.21.3.data/scripts/cleanup_kubernetes_jobs.py +344 -0
  305. paasta_tools-1.21.3.data/scripts/create_dynamodb_table.py +35 -0
  306. paasta_tools-1.21.3.data/scripts/create_paasta_playground.py +105 -0
  307. paasta_tools-1.21.3.data/scripts/delete_kubernetes_deployments.py +89 -0
  308. paasta_tools-1.21.3.data/scripts/emit_allocated_cpu_metrics.py +50 -0
  309. paasta_tools-1.21.3.data/scripts/generate_all_deployments +9 -0
  310. paasta_tools-1.21.3.data/scripts/generate_authenticating_services.py +94 -0
  311. paasta_tools-1.21.3.data/scripts/generate_deployments_for_service.py +255 -0
  312. paasta_tools-1.21.3.data/scripts/generate_services_file.py +114 -0
  313. paasta_tools-1.21.3.data/scripts/generate_services_yaml.py +30 -0
  314. paasta_tools-1.21.3.data/scripts/get_running_task_allocation.py +346 -0
  315. paasta_tools-1.21.3.data/scripts/habitat_fixer.py +86 -0
  316. paasta_tools-1.21.3.data/scripts/ide_helper.py +316 -0
  317. paasta_tools-1.21.3.data/scripts/is_pod_healthy_in_proxy.py +139 -0
  318. paasta_tools-1.21.3.data/scripts/is_pod_healthy_in_smartstack.py +50 -0
  319. paasta_tools-1.21.3.data/scripts/kill_bad_containers.py +109 -0
  320. paasta_tools-1.21.3.data/scripts/kubernetes_remove_evicted_pods.py +164 -0
  321. paasta_tools-1.21.3.data/scripts/mass-deploy-tag.sh +44 -0
  322. paasta_tools-1.21.3.data/scripts/mock_patch_checker.py +86 -0
  323. paasta_tools-1.21.3.data/scripts/paasta_cleanup_remote_run_resources.py +135 -0
  324. paasta_tools-1.21.3.data/scripts/paasta_cleanup_stale_nodes.py +181 -0
  325. paasta_tools-1.21.3.data/scripts/paasta_deploy_tron_jobs +3 -0
  326. paasta_tools-1.21.3.data/scripts/paasta_execute_docker_command.py +123 -0
  327. paasta_tools-1.21.3.data/scripts/paasta_secrets_sync.py +758 -0
  328. paasta_tools-1.21.3.data/scripts/paasta_tabcomplete.sh +23 -0
  329. paasta_tools-1.21.3.data/scripts/paasta_update_soa_memcpu.py +520 -0
  330. paasta_tools-1.21.3.data/scripts/render_template.py +129 -0
  331. paasta_tools-1.21.3.data/scripts/rightsizer_soaconfigs_update.py +348 -0
  332. paasta_tools-1.21.3.data/scripts/service_shard_remove.py +157 -0
  333. paasta_tools-1.21.3.data/scripts/service_shard_update.py +373 -0
  334. paasta_tools-1.21.3.data/scripts/setup_istio_mesh.py +353 -0
  335. paasta_tools-1.21.3.data/scripts/setup_kubernetes_cr.py +412 -0
  336. paasta_tools-1.21.3.data/scripts/setup_kubernetes_crd.py +138 -0
  337. paasta_tools-1.21.3.data/scripts/setup_kubernetes_internal_crd.py +154 -0
  338. paasta_tools-1.21.3.data/scripts/setup_kubernetes_job.py +353 -0
  339. paasta_tools-1.21.3.data/scripts/setup_prometheus_adapter_config.py +1028 -0
  340. paasta_tools-1.21.3.data/scripts/shared_ip_check.py +77 -0
  341. paasta_tools-1.21.3.data/scripts/synapse_srv_namespaces_fact.py +42 -0
  342. paasta_tools-1.21.3.data/scripts/timeouts_metrics_prom.py +64 -0
  343. paasta_tools-1.21.3.dist-info/LICENSE +201 -0
  344. paasta_tools-1.21.3.dist-info/METADATA +74 -0
  345. paasta_tools-1.21.3.dist-info/RECORD +348 -0
  346. paasta_tools-1.21.3.dist-info/WHEEL +5 -0
  347. paasta_tools-1.21.3.dist-info/entry_points.txt +20 -0
  348. paasta_tools-1.21.3.dist-info/top_level.txt +2 -0
@@ -0,0 +1,1601 @@
1
+ #!/usr/bin/env python
2
+ # Copyright 2015-2016 Yelp Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """PaaSTA log reader for humans"""
16
+ import argparse
17
+ import datetime
18
+ import json
19
+ import logging
20
+ import re
21
+ import sys
22
+ from collections import namedtuple
23
+ from contextlib import contextmanager
24
+ from multiprocessing import Process
25
+ from multiprocessing import Queue
26
+ from queue import Empty
27
+ from time import sleep
28
+ from typing import Any
29
+ from typing import Callable
30
+ from typing import ContextManager
31
+ from typing import Dict
32
+ from typing import Iterable
33
+ from typing import List
34
+ from typing import Mapping
35
+ from typing import MutableSequence
36
+ from typing import Optional
37
+ from typing import Sequence
38
+ from typing import Set
39
+ from typing import Tuple
40
+ from typing import Type
41
+ from typing import Union
42
+
43
+ import a_sync
44
+ import isodate
45
+ import nats
46
+ import pytz
47
+ from dateutil import tz
48
+
49
+ try:
50
+ from scribereader import scribereader
51
+ from scribereader.scribereader import StreamTailerSetupError
52
+ except ImportError:
53
+ scribereader = None
54
+
55
+ try:
56
+ from logreader.readers import S3LogsReader
57
+ except ImportError:
58
+ S3LogsReader = None
59
+
60
+ from pytimeparse.timeparse import timeparse
61
+
62
+ from paasta_tools.cli.utils import figure_out_service_name
63
+ from paasta_tools.cli.utils import guess_service_name
64
+ from paasta_tools.cli.utils import lazy_choices_completer
65
+ from paasta_tools.cli.utils import verify_instances
66
+ from paasta_tools.utils import list_services
67
+ from paasta_tools.utils import ANY_CLUSTER
68
+ from paasta_tools.utils import datetime_convert_timezone
69
+ from paasta_tools.utils import datetime_from_utc_to_local
70
+ from paasta_tools.utils import DEFAULT_LOGLEVEL
71
+ from paasta_tools.utils import DEFAULT_SOA_DIR
72
+ from paasta_tools.utils import load_system_paasta_config
73
+ from paasta_tools.utils import list_clusters
74
+ from paasta_tools.utils import LOG_COMPONENTS
75
+ from paasta_tools.utils import PaastaColors
76
+ from paasta_tools.utils import get_log_name_for_service
77
+
78
+
79
+ DEFAULT_COMPONENTS = ["stdout", "stderr"]
80
+
81
+ log = logging.getLogger(__name__)
82
+
83
+
84
+ def add_subparser(subparsers) -> None:
85
+ status_parser = subparsers.add_parser(
86
+ "logs",
87
+ help="Streams logs relevant to a service across the PaaSTA components",
88
+ description=(
89
+ "'paasta logs' works by streaming PaaSTA-related event messages "
90
+ "in a human-readable way."
91
+ ),
92
+ formatter_class=argparse.RawDescriptionHelpFormatter,
93
+ )
94
+ status_parser.add_argument(
95
+ "-s",
96
+ "--service",
97
+ help="The name of the service you wish to inspect. Defaults to autodetect.",
98
+ ).completer = lazy_choices_completer(list_services)
99
+ status_parser.add_argument(
100
+ "-c",
101
+ "--cluster",
102
+ help="The cluster to see relevant logs for.",
103
+ nargs=1,
104
+ ).completer = completer_clusters
105
+ status_parser.add_argument(
106
+ "-i",
107
+ "--instance",
108
+ help="The instance to see relevant logs for. Defaults to all instances for this service.",
109
+ type=str,
110
+ ).completer = completer_clusters
111
+ pod_help = (
112
+ "The pods to see relevant logs for. Defaults to all pods for this service."
113
+ )
114
+ status_parser.add_argument("-p", "--pods", help=pod_help)
115
+ status_parser.add_argument(
116
+ "-C",
117
+ "--components",
118
+ type=lambda s: set(s.split(",")),
119
+ default=set(DEFAULT_COMPONENTS),
120
+ help=(
121
+ "A comma-separated list of the components you want logs for. "
122
+ "PaaSTA consists of 'components' such as builds and deployments, "
123
+ "for each of which we collect logs for per service. "
124
+ "See below for a list of components. "
125
+ "Defaults to %(default)s."
126
+ ),
127
+ ).completer = lazy_choices_completer(LOG_COMPONENTS.keys)
128
+ status_parser.add_argument(
129
+ "-f",
130
+ "-F",
131
+ "--tail",
132
+ dest="tail",
133
+ action="store_true",
134
+ default=False,
135
+ help="Stream the logs and follow it for more data",
136
+ )
137
+ status_parser.add_argument(
138
+ "-v",
139
+ "--verbose",
140
+ action="store_true",
141
+ dest="verbose",
142
+ default=False,
143
+ help="Enable verbose logging",
144
+ )
145
+ status_parser.add_argument(
146
+ "-r",
147
+ "--raw-mode",
148
+ action="store_true",
149
+ dest="raw_mode",
150
+ default=False,
151
+ help="Don't pretty-print logs; emit them exactly as they are in scribe.",
152
+ )
153
+ status_parser.add_argument(
154
+ "-d",
155
+ "--soa-dir",
156
+ dest="soa_dir",
157
+ metavar="SOA_DIR",
158
+ default=DEFAULT_SOA_DIR,
159
+ help=f"Define a different soa config directory. Defaults to %(default)s.",
160
+ )
161
+
162
+ status_parser.add_argument(
163
+ "-a",
164
+ "--from",
165
+ "--after",
166
+ dest="time_from",
167
+ help=(
168
+ "The time to start getting logs from. "
169
+ 'This can be an ISO-8601 timestamp or a human readable duration parsable by pytimeparse such as "5m", "1d3h" etc. '
170
+ 'For example: --from "3m" would start retrieving logs from 3 minutes ago. '
171
+ "Incompatible with --line-offset and --lines."
172
+ ),
173
+ )
174
+ status_parser.add_argument(
175
+ "-t",
176
+ "--to",
177
+ dest="time_to",
178
+ help=(
179
+ "The time to get logs up to. "
180
+ 'This can be an ISO-8601 timestamp or a human readable duration parsable by pytimeparse such as "5m", "1d3h" etc. '
181
+ "Incompatiable with --line-offset and --lines. "
182
+ "Defaults to now."
183
+ ),
184
+ )
185
+ status_parser.add_argument(
186
+ "-l",
187
+ "-n",
188
+ "--lines",
189
+ dest="line_count",
190
+ help=(
191
+ "The number of lines to retrieve from the specified offset. "
192
+ 'May optionally be prefixed with a "+" or "-" to specify which direction from the offset. '
193
+ "Incompatiable with --from and --to. "
194
+ 'Defaults to "-100".'
195
+ ),
196
+ type=int,
197
+ )
198
+ status_parser.add_argument(
199
+ "-o",
200
+ "--line-offset",
201
+ dest="line_offset",
202
+ help=(
203
+ "The offset at which line to start grabbing logs from. "
204
+ "For example, --line-offset 1 would be the first line. "
205
+ "Paired with --lines, --line-offset +100 would give you the first 100 lines of logs. "
206
+ "Some logging backends may not support line offsetting by time or lines. "
207
+ "Incompatiable with --from and --to. "
208
+ "Defaults to the latest line's offset."
209
+ ),
210
+ type=int,
211
+ )
212
+ status_parser.add_argument(
213
+ "-S",
214
+ "--strip-headers",
215
+ dest="strip_headers",
216
+ help="Print log lines without header information.",
217
+ action="store_true",
218
+ )
219
+ status_parser.epilog = (
220
+ "COMPONENTS\n"
221
+ "Here is a list of all components and what they are:\n"
222
+ f"{build_component_descriptions(LOG_COMPONENTS)}"
223
+ )
224
+ status_parser.set_defaults(command=paasta_logs)
225
+
226
+
227
+ def completer_clusters(prefix, parsed_args, **kwargs):
228
+ service = parsed_args.service or guess_service_name()
229
+ if service in list_services():
230
+ return list_clusters(service)
231
+ else:
232
+ return list_clusters()
233
+
234
+
235
+ def build_component_descriptions(components: Mapping[str, Mapping[str, Any]]) -> str:
236
+ """Returns a colored description string for every log component
237
+ based on its help attribute"""
238
+ output = []
239
+ for k, v in components.items():
240
+ output.append(" {}: {}".format(v["color"](k), v["help"]))
241
+ return "\n".join(output)
242
+
243
+
244
+ def prefix(input_string: str, component: str) -> str:
245
+ """Returns a colored string with the right colored prefix with a given component"""
246
+ return "{}: {}".format(LOG_COMPONENTS[component]["color"](component), input_string)
247
+
248
+
249
+ # The reason this returns true if start_time or end_time are None is because
250
+ # filtering by time is optional, and it allows us to simply do
251
+ # if not check_timestamp_in_range(...): return False
252
+ # The default arguments for start_time and end_time are None when we aren't
253
+ # filtering by time
254
+ def check_timestamp_in_range(
255
+ timestamp: datetime.datetime,
256
+ start_time: datetime.datetime,
257
+ end_time: datetime.datetime,
258
+ ) -> bool:
259
+ """A convenience function to check if a datetime.datetime timestamp is within the given start and end times,
260
+ returns true if start_time or end_time is None
261
+
262
+ :param timestamp: The timestamp to check
263
+ :param start_time: The start of the interval
264
+ :param end_time: The end of the interval
265
+ :return: True if timestamp is within start_time and end_time range, False otherwise
266
+ """
267
+ if timestamp is not None and start_time is not None and end_time is not None:
268
+ if timestamp.tzinfo is None:
269
+ timestamp = pytz.utc.localize(timestamp)
270
+ return start_time < timestamp < end_time
271
+ else:
272
+ return True
273
+
274
+
275
+ def paasta_log_line_passes_filter(
276
+ line: str,
277
+ levels: Sequence[str],
278
+ service: str,
279
+ components: Iterable[str],
280
+ clusters: Sequence[str],
281
+ instances: List[str],
282
+ pods: Iterable[str] = None,
283
+ start_time: datetime.datetime = None,
284
+ end_time: datetime.datetime = None,
285
+ ) -> bool:
286
+ """Given a (JSON-formatted) log line, return True if the line should be
287
+ displayed given the provided levels, components, and clusters; return False
288
+ otherwise.
289
+
290
+ NOTE: Pods are optional as services that use Mesos do not operate with pods.
291
+ """
292
+ try:
293
+ parsed_line = json.loads(line)
294
+ except ValueError:
295
+ log.debug("Trouble parsing line as json. Skipping. Line: %r" % line)
296
+ return False
297
+
298
+ if (
299
+ (instances is None or parsed_line.get("instance") in instances)
300
+ and (parsed_line.get("level") is None or parsed_line.get("level") in levels)
301
+ and parsed_line.get("component") in components
302
+ and (
303
+ parsed_line.get("cluster") in clusters
304
+ or parsed_line.get("cluster") == ANY_CLUSTER
305
+ )
306
+ ):
307
+ timestamp = isodate.parse_datetime(parsed_line.get("timestamp"))
308
+ if check_timestamp_in_range(timestamp, start_time, end_time):
309
+ return True
310
+ return False
311
+
312
+
313
+ def paasta_app_output_passes_filter(
314
+ line: str,
315
+ levels: Sequence[str],
316
+ service: str,
317
+ components: Iterable[str],
318
+ clusters: Sequence[str],
319
+ instances: List[str],
320
+ pods: Iterable[str] = None,
321
+ start_time: datetime.datetime = None,
322
+ end_time: datetime.datetime = None,
323
+ ) -> bool:
324
+ try:
325
+ parsed_line = json.loads(line)
326
+ except ValueError:
327
+ log.debug("Trouble parsing line as json. Skipping. Line: %r" % line)
328
+ return False
329
+
330
+ if (
331
+ (instances is None or parsed_line.get("instance") in instances)
332
+ and parsed_line.get("cluster") in clusters
333
+ and parsed_line.get("component") in components
334
+ and (pods is None or parsed_line.get("pod_name") in pods)
335
+ ):
336
+ try:
337
+ timestamp = isodate.parse_datetime(parsed_line.get("timestamp"))
338
+ except AttributeError:
339
+ # Timestamp might be missing. We had an issue where OTel was splitting overly long log lines
340
+ # and not including timestamps in the resulting log records (OBSPLAT-2216).
341
+ # Although this was then fixed in OTel, we should not rely on timestamps being present,
342
+ # as the format cannot be guaranteed.
343
+ return False
344
+ if check_timestamp_in_range(timestamp, start_time, end_time):
345
+ return True
346
+ return False
347
+
348
+
349
+ def extract_utc_timestamp_from_log_line(line: str) -> datetime.datetime:
350
+ """
351
+ Extracts the timestamp from a log line of the format "<timestamp> <other data>" and returns a UTC datetime object
352
+ or None if it could not parse the line
353
+ """
354
+ # Extract ISO 8601 date per http://www.pelagodesign.com/blog/2009/05/20/iso-8601-date-validation-that-doesnt-suck/
355
+ iso_re = (
356
+ r"^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|"
357
+ r"(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+"
358
+ r"(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)? "
359
+ )
360
+
361
+ tokens = re.match(iso_re, line)
362
+
363
+ if not tokens:
364
+ # Could not parse line
365
+ return None
366
+ timestamp = tokens.group(0).strip()
367
+ dt = isodate.parse_datetime(timestamp)
368
+ utc_timestamp = datetime_convert_timezone(dt, dt.tzinfo, tz.tzutc())
369
+ return utc_timestamp
370
+
371
+
372
+ def print_log(
373
+ line: str,
374
+ requested_levels: Sequence[str],
375
+ raw_mode: bool = False,
376
+ strip_headers: bool = False,
377
+ ) -> None:
378
+ """Mostly a stub to ease testing. Eventually this may do some formatting or
379
+ something.
380
+ """
381
+ if raw_mode:
382
+ # suppress trailing newline since scribereader already attached one
383
+ print(line, end=" ", flush=True)
384
+ else:
385
+ print(
386
+ prettify_log_line(line, requested_levels, strip_headers),
387
+ flush=True,
388
+ )
389
+
390
+
391
+ def prettify_timestamp(timestamp: datetime.datetime) -> str:
392
+ """Returns more human-friendly form of 'timestamp' without microseconds and
393
+ in local time.
394
+ """
395
+ dt = isodate.parse_datetime(timestamp)
396
+ pretty_timestamp = datetime_from_utc_to_local(dt)
397
+ return pretty_timestamp.strftime("%Y-%m-%d %H:%M:%S")
398
+
399
+
400
+ def prettify_component(component: str) -> str:
401
+ try:
402
+ return LOG_COMPONENTS[component]["color"]("[%s]" % component)
403
+ except KeyError:
404
+ return "UNPRETTIFIABLE COMPONENT %s" % component
405
+
406
+
407
+ def prettify_level(level: str, requested_levels: Sequence[str]) -> str:
408
+ """Colorize level. 'event' is special and gets bolded; everything else gets
409
+ lightened.
410
+
411
+ requested_levels is an iterable of levels that will be displayed. If only
412
+ one level will be displayed, don't bother to print it (return empty string).
413
+ If multiple levels will be displayed, emit the (prettified) level so the
414
+ resulting log output is not ambiguous.
415
+ """
416
+ pretty_level = ""
417
+ if len(requested_levels) > 1:
418
+ if level == "event":
419
+ pretty_level = PaastaColors.bold("[%s] " % level)
420
+ else:
421
+ pretty_level = PaastaColors.grey("[%s] " % level)
422
+ return pretty_level
423
+
424
+
425
+ def prettify_log_line(
426
+ line: str, requested_levels: Sequence[str], strip_headers: bool
427
+ ) -> str:
428
+ """Given a line from the log, which is expected to be JSON and have all the
429
+ things we expect, return a pretty formatted string containing relevant values.
430
+ """
431
+ try:
432
+ parsed_line = json.loads(line)
433
+ except ValueError:
434
+ log.debug("Trouble parsing line as json. Skipping. Line: %r" % line)
435
+ return "Invalid JSON: %s" % line
436
+
437
+ try:
438
+ if strip_headers:
439
+ return "%(timestamp)s %(message)s" % (
440
+ {
441
+ "timestamp": prettify_timestamp(parsed_line["timestamp"]),
442
+ "message": parsed_line["message"],
443
+ }
444
+ )
445
+ else:
446
+ return "%(timestamp)s %(component)s - %(message)s" % (
447
+ {
448
+ "timestamp": prettify_timestamp(parsed_line["timestamp"]),
449
+ "component": prettify_component(parsed_line["component"]),
450
+ "message": parsed_line["message"],
451
+ }
452
+ )
453
+ except KeyError:
454
+ log.debug(
455
+ "JSON parsed correctly but was missing a key. Skipping. Line: %r" % line
456
+ )
457
+ return "JSON missing keys: %s" % line
458
+
459
+
460
+ # The map of name -> LogReader subclasses, used by configure_log.
461
+ _log_reader_classes = {}
462
+
463
+
464
+ def register_log_reader(name):
465
+ """Returns a decorator that registers a log reader class at a given name
466
+ so get_log_reader_classes can find it."""
467
+
468
+ def outer(log_reader_class):
469
+ _log_reader_classes[name] = log_reader_class
470
+ return log_reader_class
471
+
472
+ return outer
473
+
474
+
475
+ def get_log_reader_class(
476
+ name: str,
477
+ ) -> Union[Type["ScribeLogReader"], Type["VectorLogsReader"]]:
478
+ return _log_reader_classes[name]
479
+
480
+
481
+ def list_log_readers() -> Iterable[str]:
482
+ return _log_reader_classes.keys()
483
+
484
+
485
+ def get_log_reader(components: Set[str]) -> "LogReader":
486
+ log_readers_config = load_system_paasta_config().get_log_readers()
487
+ # ideally we should use a single "driver" for all components, but in cases where more than one is used for different components,
488
+ # we should use the first one that supports all requested components
489
+ # otherwise signal an error and suggest to provide a different list of components
490
+ components_lists = []
491
+ for log_reader_config in log_readers_config:
492
+ supported_components = log_reader_config.get("components", [])
493
+ components_lists.append(supported_components)
494
+ if components.issubset(supported_components):
495
+ log_reader_class = get_log_reader_class(log_reader_config["driver"])
496
+ print(
497
+ PaastaColors.cyan(
498
+ "Using '{}' driver to fetch logs...".format(
499
+ log_reader_config["driver"]
500
+ )
501
+ ),
502
+ file=sys.stderr,
503
+ )
504
+ return log_reader_class(**log_reader_config.get("options", {}))
505
+ print(
506
+ PaastaColors.cyan(
507
+ "Supplied list of components '{}' is not supported by any log reader. Supported components are:\n\t{}".format(
508
+ ", ".join(components),
509
+ "\n\tor ".join([",".join(comp_list) for comp_list in components_lists]),
510
+ )
511
+ ),
512
+ file=sys.stderr,
513
+ )
514
+ sys.exit(1)
515
+
516
+
517
+ class LogReader:
518
+ # Tailing, i.e actively viewing logs as they come in
519
+ SUPPORTS_TAILING = False
520
+ # Getting the last n lines of logs
521
+ SUPPORTS_LINE_COUNT = False
522
+ # Getting the last/prev n lines of logs from line #34013 for example
523
+ SUPPORTS_LINE_OFFSET = False
524
+ # Getting the logs between two given times
525
+ SUPPORTS_TIME = False
526
+ # Supporting at least one of these log retrieval modes is required
527
+
528
+ def tail_logs(
529
+ self,
530
+ service,
531
+ levels,
532
+ components,
533
+ clusters,
534
+ instances,
535
+ pods,
536
+ raw_mode=False,
537
+ strip_headers=False,
538
+ ):
539
+ raise NotImplementedError("tail_logs is not implemented")
540
+
541
+ def print_logs_by_time(
542
+ self,
543
+ service,
544
+ start_time,
545
+ end_time,
546
+ levels,
547
+ components,
548
+ clusters,
549
+ instances,
550
+ pods,
551
+ raw_mode,
552
+ strip_headers,
553
+ ):
554
+ raise NotImplementedError("print_logs_by_time is not implemented")
555
+
556
+ def print_last_n_logs(
557
+ self,
558
+ service,
559
+ line_count,
560
+ levels,
561
+ components,
562
+ clusters,
563
+ instances,
564
+ pods,
565
+ raw_mode,
566
+ strip_headers,
567
+ ):
568
+ raise NotImplementedError("print_last_n_logs is not implemented")
569
+
570
+ def print_logs_by_offset(
571
+ self,
572
+ service,
573
+ line_count,
574
+ line_offset,
575
+ levels,
576
+ components,
577
+ clusters,
578
+ instances,
579
+ pods,
580
+ raw_mode,
581
+ strip_headers,
582
+ ):
583
+ raise NotImplementedError("print_logs_by_offset is not implemented")
584
+
585
+
586
+ ScribeComponentStreamInfo = namedtuple(
587
+ "ScribeComponentStreamInfo", "per_cluster, stream_name_fn, filter_fn, parse_fn"
588
+ )
589
+
590
+
591
+ @register_log_reader("scribereader")
592
+ class ScribeLogReader(LogReader):
593
+ SUPPORTS_TAILING = True
594
+ SUPPORTS_LINE_COUNT = True
595
+ SUPPORTS_TIME = True
596
+
597
+ COMPONENT_STREAM_INFO = {
598
+ "default": ScribeComponentStreamInfo(
599
+ per_cluster=False,
600
+ stream_name_fn=get_log_name_for_service,
601
+ filter_fn=paasta_log_line_passes_filter,
602
+ parse_fn=None,
603
+ ),
604
+ "stdout": ScribeComponentStreamInfo(
605
+ per_cluster=False,
606
+ stream_name_fn=lambda service: get_log_name_for_service(
607
+ service, prefix="app_output"
608
+ ),
609
+ filter_fn=paasta_app_output_passes_filter,
610
+ parse_fn=None,
611
+ ),
612
+ "stderr": ScribeComponentStreamInfo(
613
+ per_cluster=False,
614
+ stream_name_fn=lambda service: get_log_name_for_service(
615
+ service, prefix="app_output"
616
+ ),
617
+ filter_fn=paasta_app_output_passes_filter,
618
+ parse_fn=None,
619
+ ),
620
+ }
621
+
622
+ def __init__(self, cluster_map: Mapping[str, Any]) -> None:
623
+ super().__init__()
624
+
625
+ if scribereader is None:
626
+ raise Exception(
627
+ "scribereader package must be available to use scribereader log reading backend"
628
+ )
629
+ self.cluster_map = cluster_map
630
+
631
+ def get_scribereader_selector(self, scribe_env: str) -> str:
632
+ # this is kinda silly, but until the scribereader cli becomes more ergonomic
633
+ # we'll need to do a little bit of string munging to let humans use scribereader
634
+ # in the same way we are (tl;dr: scribereader has sorta confusing behavior between
635
+ # what can be use for --ecosystem, --region, and --superregion and the fastest/least
636
+ # hacky thing to figure out which we wanna use is that any env with a - in it is a region
637
+ # and any without one is an ecosystem)
638
+ return "-e" if "-" in scribe_env else "-r"
639
+
640
+ def run_code_over_scribe_envs(
641
+ self,
642
+ clusters: Sequence[str],
643
+ components: Iterable[str],
644
+ callback: Callable[..., None],
645
+ ) -> None:
646
+ """Iterates over the scribe environments for a given set of clusters and components, executing
647
+ functions for each component
648
+
649
+ :param clusters: The set of clusters
650
+ :param components: The set of components
651
+ :param callback: The callback function. Gets called with (component_name, stream_info, scribe_env, cluster)
652
+ The cluster field will only be set if the component is set to per_cluster
653
+ """
654
+ scribe_envs: Set[str] = set()
655
+ for cluster in clusters:
656
+ scribe_envs.update(self.determine_scribereader_envs(components, cluster))
657
+ log.debug("Connect to these scribe envs to tail scribe logs: %s" % scribe_envs)
658
+
659
+ for scribe_env in scribe_envs:
660
+ # These components all get grouped in one call for backwards compatibility
661
+ grouped_components = {"build", "deploy", "monitoring"}
662
+
663
+ if any([component in components for component in grouped_components]):
664
+ stream_info = self.get_stream_info("default")
665
+ callback(components, stream_info, scribe_env, cluster=None)
666
+
667
+ non_defaults = set(components) - grouped_components
668
+ for component in non_defaults:
669
+ stream_info = self.get_stream_info(component)
670
+
671
+ if stream_info.per_cluster:
672
+ for cluster in clusters:
673
+ callback([component], stream_info, scribe_env, cluster=cluster)
674
+ else:
675
+ callback([component], stream_info, scribe_env, cluster=None)
676
+
677
+ def get_stream_info(self, component: str) -> ScribeComponentStreamInfo:
678
+ if component in self.COMPONENT_STREAM_INFO:
679
+ return self.COMPONENT_STREAM_INFO[component]
680
+ else:
681
+ return self.COMPONENT_STREAM_INFO["default"]
682
+
683
+ def tail_logs(
684
+ self,
685
+ service: str,
686
+ levels: Sequence[str],
687
+ components: Iterable[str],
688
+ clusters: Sequence[str],
689
+ instances: List[str],
690
+ pods: Iterable[str] = None,
691
+ raw_mode: bool = False,
692
+ strip_headers: bool = False,
693
+ ) -> None:
694
+ """Sergeant function for spawning off all the right log tailing functions.
695
+
696
+ NOTE: This function spawns concurrent processes and doesn't necessarily
697
+ worry about cleaning them up! That's because we expect to just exit the
698
+ main process when this function returns (as main() does). Someone calling
699
+ this function directly with something like "while True: tail_paasta_logs()"
700
+ may be very sad.
701
+
702
+ NOTE: We try pretty hard to suppress KeyboardInterrupts to prevent big
703
+ useless stack traces, but it turns out to be non-trivial and we fail ~10%
704
+ of the time. We decided we could live with it and we're shipping this to
705
+ see how it fares in real world testing.
706
+
707
+ Here are some things we read about this problem:
708
+ * http://stackoverflow.com/questions/1408356/keyboard-interrupts-with-pythons-multiprocessing-pool
709
+ * http://jtushman.github.io/blog/2014/01/14/python-%7C-multiprocessing-and-interrupts/
710
+ * http://bryceboe.com/2010/08/26/python-multiprocessing-and-keyboardinterrupt/
711
+
712
+ We could also try harder to terminate processes from more places. We could
713
+ use process.join() to ensure things have a chance to die. We punted these
714
+ things.
715
+
716
+ It's possible this whole multiprocessing strategy is wrong-headed. If you
717
+ are reading this code to curse whoever wrote it, see discussion in
718
+ PAASTA-214 and https://reviewboard.yelpcorp.com/r/87320/ and feel free to
719
+ implement one of the other options.
720
+ """
721
+ queue: Queue = Queue()
722
+ spawned_processes = []
723
+
724
+ def callback(
725
+ components: Iterable[str],
726
+ stream_info: ScribeComponentStreamInfo,
727
+ scribe_env: str,
728
+ cluster: str,
729
+ ) -> None:
730
+ kw = {
731
+ "scribe_env": scribe_env,
732
+ "service": service,
733
+ "levels": levels,
734
+ "components": components,
735
+ "clusters": clusters,
736
+ "instances": instances,
737
+ "pods": pods,
738
+ "queue": queue,
739
+ "filter_fn": stream_info.filter_fn,
740
+ }
741
+
742
+ if stream_info.per_cluster:
743
+ kw["stream_name"] = stream_info.stream_name_fn(service, cluster)
744
+ kw["clusters"] = [cluster]
745
+ else:
746
+ kw["stream_name"] = stream_info.stream_name_fn(service)
747
+ log.debug(
748
+ "Running the equivalent of 'scribereader {} {} {}'".format(
749
+ self.get_scribereader_selector(scribe_env),
750
+ scribe_env,
751
+ kw["stream_name"],
752
+ )
753
+ )
754
+ process = Process(target=self.scribe_tail, kwargs=kw)
755
+ spawned_processes.append(process)
756
+ process.start()
757
+
758
+ self.run_code_over_scribe_envs(
759
+ clusters=clusters, components=components, callback=callback
760
+ )
761
+
762
+ # Pull things off the queue and output them. If any thread dies we are no
763
+ # longer presenting the user with the full picture so we quit.
764
+ #
765
+ # This is convenient for testing, where a fake scribe_tail() can emit a
766
+ # fake log and exit. Without the thread aliveness check, we would just sit
767
+ # here forever even though the threads doing the tailing are all gone.
768
+ #
769
+ # NOTE: A noisy tailer in one scribe_env (such that the queue never gets
770
+ # empty) will prevent us from ever noticing that another tailer has died.
771
+ while True:
772
+ try:
773
+ # This is a blocking call with a timeout for a couple reasons:
774
+ #
775
+ # * If the queue is empty and we get_nowait(), we loop very tightly
776
+ # and accomplish nothing.
777
+ #
778
+ # * Testing revealed a race condition where print_log() is called
779
+ # and even prints its message, but this action isn't recorded on
780
+ # the patched-in print_log(). This resulted in test flakes. A short
781
+ # timeout seems to soothe this behavior: running this test 10 times
782
+ # with a timeout of 0.0 resulted in 2 failures; running it with a
783
+ # timeout of 0.1 resulted in 0 failures.
784
+ #
785
+ # * There's a race where thread1 emits its log line and exits
786
+ # before thread2 has a chance to do anything, causing us to bail
787
+ # out via the Queue Empty and thread aliveness check.
788
+ #
789
+ # We've decided to live with this for now and see if it's really a
790
+ # problem. The threads in test code exit pretty much immediately
791
+ # and a short timeout has been enough to ensure correct behavior
792
+ # there, so IRL with longer start-up times for each thread this
793
+ # will surely be fine.
794
+ #
795
+ # UPDATE: Actually this is leading to a test failure rate of about
796
+ # 1/10 even with timeout of 1s. I'm adding a sleep to the threads
797
+ # in test code to smooth this out, then pulling the trigger on
798
+ # moving that test to integration land where it belongs.
799
+ line = queue.get(block=True, timeout=0.1)
800
+ print_log(line, levels, raw_mode, strip_headers)
801
+ except Empty:
802
+ try:
803
+ # If there's nothing in the queue, take this opportunity to make
804
+ # sure all the tailers are still running.
805
+ running_processes = [tt.is_alive() for tt in spawned_processes]
806
+ if not running_processes or not all(running_processes):
807
+ log.warn(
808
+ "Quitting because I expected %d log tailers to be alive but only %d are alive."
809
+ % (len(spawned_processes), running_processes.count(True))
810
+ )
811
+ for process in spawned_processes:
812
+ if process.is_alive():
813
+ process.terminate()
814
+ break
815
+ except KeyboardInterrupt:
816
+ # Die peacefully rather than printing N threads worth of stack
817
+ # traces.
818
+ #
819
+ # This extra nested catch is because it's pretty easy to be in
820
+ # the above try block when the user hits Ctrl-C which otherwise
821
+ # dumps a stack trace.
822
+ log.warn("Terminating.")
823
+ break
824
+ except KeyboardInterrupt:
825
+ # Die peacefully rather than printing N threads worth of stack
826
+ # traces.
827
+ log.warn("Terminating.")
828
+ break
829
+
830
+ def print_logs_by_time(
831
+ self,
832
+ service: str,
833
+ start_time: datetime.datetime,
834
+ end_time: datetime.datetime,
835
+ levels: Sequence[str],
836
+ components: Iterable[str],
837
+ clusters: Sequence[str],
838
+ instances: List[str],
839
+ pods: Iterable[str],
840
+ raw_mode: bool,
841
+ strip_headers: bool,
842
+ ) -> None:
843
+ aggregated_logs: List[Dict[str, Any]] = []
844
+
845
+ def callback(
846
+ components: Iterable[str],
847
+ stream_info: ScribeComponentStreamInfo,
848
+ scribe_env: str,
849
+ cluster: str,
850
+ ) -> None:
851
+ if stream_info.per_cluster:
852
+ stream_name = stream_info.stream_name_fn(service, cluster)
853
+ else:
854
+ stream_name = stream_info.stream_name_fn(service)
855
+
856
+ ctx = self.scribe_get_from_time(
857
+ scribe_env, stream_name, start_time, end_time
858
+ )
859
+ self.filter_and_aggregate_scribe_logs(
860
+ scribe_reader_ctx=ctx,
861
+ scribe_env=scribe_env,
862
+ stream_name=stream_name,
863
+ levels=levels,
864
+ service=service,
865
+ components=components,
866
+ clusters=clusters,
867
+ instances=instances,
868
+ aggregated_logs=aggregated_logs,
869
+ pods=pods,
870
+ filter_fn=stream_info.filter_fn,
871
+ parser_fn=stream_info.parse_fn,
872
+ start_time=start_time,
873
+ end_time=end_time,
874
+ )
875
+
876
+ self.run_code_over_scribe_envs(
877
+ clusters=clusters, components=components, callback=callback
878
+ )
879
+
880
+ aggregated_logs = list(
881
+ {line["raw_line"]: line for line in aggregated_logs}.values()
882
+ )
883
+ aggregated_logs.sort(key=lambda log_line: log_line["sort_key"])
884
+
885
+ for line in aggregated_logs:
886
+ print_log(line["raw_line"], levels, raw_mode, strip_headers)
887
+
888
+ def print_last_n_logs(
889
+ self,
890
+ service: str,
891
+ line_count: int,
892
+ levels: Sequence[str],
893
+ components: Iterable[str],
894
+ clusters: Sequence[str],
895
+ instances: List[str],
896
+ pods: Iterable[str],
897
+ raw_mode: bool,
898
+ strip_headers: bool,
899
+ ) -> None:
900
+ aggregated_logs: List[Dict[str, Any]] = []
901
+
902
+ def callback(
903
+ components: Iterable[str],
904
+ stream_info: ScribeComponentStreamInfo,
905
+ scribe_env: str,
906
+ cluster: str,
907
+ ) -> None:
908
+
909
+ if stream_info.per_cluster:
910
+ stream_name = stream_info.stream_name_fn(service, cluster)
911
+ else:
912
+ stream_name = stream_info.stream_name_fn(service)
913
+
914
+ ctx = self.scribe_get_last_n_lines(scribe_env, stream_name, line_count)
915
+ self.filter_and_aggregate_scribe_logs(
916
+ scribe_reader_ctx=ctx,
917
+ scribe_env=scribe_env,
918
+ stream_name=stream_name,
919
+ levels=levels,
920
+ service=service,
921
+ components=components,
922
+ clusters=clusters,
923
+ instances=instances,
924
+ aggregated_logs=aggregated_logs,
925
+ pods=pods,
926
+ filter_fn=stream_info.filter_fn,
927
+ parser_fn=stream_info.parse_fn,
928
+ )
929
+
930
+ self.run_code_over_scribe_envs(
931
+ clusters=clusters, components=components, callback=callback
932
+ )
933
+ aggregated_logs = list(
934
+ {line["raw_line"]: line for line in aggregated_logs}.values()
935
+ )
936
+ aggregated_logs.sort(key=lambda log_line: log_line["sort_key"])
937
+
938
+ for line in aggregated_logs:
939
+ print_log(line["raw_line"], levels, raw_mode, strip_headers)
940
+
941
+ def filter_and_aggregate_scribe_logs(
942
+ self,
943
+ scribe_reader_ctx: ContextManager,
944
+ scribe_env: str,
945
+ stream_name: str,
946
+ levels: Sequence[str],
947
+ service: str,
948
+ components: Iterable[str],
949
+ clusters: Sequence[str],
950
+ instances: List[str],
951
+ aggregated_logs: MutableSequence[Dict[str, Any]],
952
+ pods: Iterable[str] = None,
953
+ parser_fn: Callable = None,
954
+ filter_fn: Callable = None,
955
+ start_time: datetime.datetime = None,
956
+ end_time: datetime.datetime = None,
957
+ ) -> None:
958
+ with scribe_reader_ctx as scribe_reader:
959
+ try:
960
+ for line in scribe_reader:
961
+ # temporary until all log lines are strings not byte strings
962
+ if isinstance(line, bytes):
963
+ line = line.decode("utf-8")
964
+ if parser_fn:
965
+ line = parser_fn(line, clusters, service)
966
+ if filter_fn:
967
+ if filter_fn(
968
+ line,
969
+ levels,
970
+ service,
971
+ components,
972
+ clusters,
973
+ instances,
974
+ pods,
975
+ start_time=start_time,
976
+ end_time=end_time,
977
+ ):
978
+ try:
979
+ parsed_line = json.loads(line)
980
+ timestamp = isodate.parse_datetime(
981
+ parsed_line.get("timestamp")
982
+ )
983
+ if not timestamp.tzinfo:
984
+ timestamp = pytz.utc.localize(timestamp)
985
+ except ValueError:
986
+ timestamp = pytz.utc.localize(datetime.datetime.min)
987
+
988
+ line = {"raw_line": line, "sort_key": timestamp}
989
+ aggregated_logs.append(line)
990
+ except StreamTailerSetupError as e:
991
+ if "No data in stream" in str(e):
992
+ log.warning(f"Scribe stream {stream_name} is empty on {scribe_env}")
993
+ log.warning(
994
+ "Don't Panic! This may or may not be a problem depending on if you expect there to be"
995
+ )
996
+ log.warning("output within this stream.")
997
+ elif "Failed to connect" in str(e):
998
+ log.warning(
999
+ f"Couldn't connect to Scribe to tail {stream_name} in {scribe_env}"
1000
+ )
1001
+ log.warning(f"Please be in {scribe_env} to tail this log.")
1002
+ else:
1003
+ raise
1004
+
1005
+ def scribe_get_from_time(
1006
+ self,
1007
+ scribe_env: str,
1008
+ stream_name: str,
1009
+ start_time: datetime.datetime,
1010
+ end_time: datetime.datetime,
1011
+ ) -> ContextManager:
1012
+ # Scribe connection details
1013
+ host, port = scribereader.get_tail_host_and_port(
1014
+ **scribe_env_to_locations(scribe_env),
1015
+ )
1016
+
1017
+ # Recent logs might not be archived yet. Log warning message.
1018
+ warning_end_time = datetime.datetime.utcnow().replace(
1019
+ tzinfo=pytz.utc
1020
+ ) - datetime.timedelta(hours=4)
1021
+ if end_time > warning_end_time:
1022
+ log.warn("Recent logs might be incomplete. Consider tailing instead.")
1023
+
1024
+ # scribereader, sadly, is not based on UTC timestamps. It uses YST
1025
+ # dates instead.
1026
+ start_date_yst = start_time.astimezone(
1027
+ pytz.timezone("America/Los_Angeles")
1028
+ ).date()
1029
+ end_date_yst = end_time.astimezone(pytz.timezone("America/Los_Angeles")).date()
1030
+
1031
+ log.debug(
1032
+ "Running the equivalent of 'scribereader %s %s %s --min-date %s --max-date %s"
1033
+ % (
1034
+ self.get_scribereader_selector(scribe_env),
1035
+ scribe_env,
1036
+ stream_name,
1037
+ start_date_yst,
1038
+ end_date_yst,
1039
+ )
1040
+ )
1041
+ return scribereader.get_stream_reader(
1042
+ stream_name=stream_name,
1043
+ reader_host=host,
1044
+ reader_port=port,
1045
+ min_date=start_date_yst,
1046
+ max_date=end_date_yst,
1047
+ )
1048
+
1049
+ def scribe_get_last_n_lines(
1050
+ self, scribe_env: str, stream_name: str, line_count: int
1051
+ ) -> ContextManager:
1052
+ # Scribe connection details
1053
+ host, port = scribereader.get_tail_host_and_port(
1054
+ **scribe_env_to_locations(scribe_env),
1055
+ )
1056
+
1057
+ # The reason we need a fake context here is because scribereader is a bit inconsistent in its
1058
+ # returns. get_stream_reader returns a context that needs to be acquired for cleanup code but
1059
+ # get_stream_tailer simply returns an object that can be iterated over. We'd still like to have
1060
+ # the cleanup code for get_stream_reader to be executed by this function's caller and this is
1061
+ # one of the simpler ways to achieve it without having 2 if statements everywhere that calls
1062
+ # this method
1063
+ @contextmanager
1064
+ def fake_context():
1065
+ log.debug(
1066
+ f"Running the equivalent of 'scribereader -n {line_count} {self.get_scribereader_selector(scribe_env)} {scribe_env} {stream_name}'"
1067
+ )
1068
+ yield scribereader.get_stream_tailer(
1069
+ stream_name=stream_name,
1070
+ tailing_host=host,
1071
+ tailing_port=port,
1072
+ lines=line_count,
1073
+ )
1074
+
1075
+ return fake_context()
1076
+
1077
+ def scribe_tail(
1078
+ self,
1079
+ scribe_env: str,
1080
+ stream_name: str,
1081
+ service: str,
1082
+ levels: Sequence[str],
1083
+ components: Iterable[str],
1084
+ clusters: Sequence[str],
1085
+ instances: List[str],
1086
+ pods: Iterable[str],
1087
+ queue: Queue,
1088
+ filter_fn: Callable,
1089
+ parse_fn: Callable = None,
1090
+ ) -> None:
1091
+ """Creates a scribetailer for a particular environment.
1092
+
1093
+ When it encounters a line that it should report, it sticks it into the
1094
+ provided queue.
1095
+
1096
+ This code is designed to run in a thread as spawned by tail_paasta_logs().
1097
+ """
1098
+ try:
1099
+ log.debug(f"Going to tail {stream_name} scribe stream in {scribe_env}")
1100
+ host, port = scribereader.get_tail_host_and_port(
1101
+ **scribe_env_to_locations(scribe_env),
1102
+ )
1103
+ tailer = scribereader.get_stream_tailer(stream_name, host, port)
1104
+ for line in tailer:
1105
+ if parse_fn:
1106
+ line = parse_fn(line, clusters, service)
1107
+ if filter_fn(
1108
+ line, levels, service, components, clusters, instances, pods
1109
+ ):
1110
+ queue.put(line)
1111
+ except KeyboardInterrupt:
1112
+ # Die peacefully rather than printing N threads worth of stack
1113
+ # traces.
1114
+ pass
1115
+ except StreamTailerSetupError as e:
1116
+ if "No data in stream" in str(e):
1117
+ log.warning(f"Scribe stream {stream_name} is empty on {scribe_env}")
1118
+ log.warning(
1119
+ "Don't Panic! This may or may not be a problem depending on if you expect there to be"
1120
+ )
1121
+ log.warning("output within this stream.")
1122
+ # Enter a wait so the process isn't considered dead.
1123
+ # This is just a large number, since apparently some python interpreters
1124
+ # don't like being passed sys.maxsize.
1125
+ sleep(2**16)
1126
+ else:
1127
+ raise
1128
+
1129
+ def determine_scribereader_envs(
1130
+ self, components: Iterable[str], cluster: str
1131
+ ) -> Set[str]:
1132
+ """Returns a list of environments that scribereader needs to connect
1133
+ to based on a given list of components and the cluster involved.
1134
+
1135
+ Some components are in certain environments, regardless of the cluster.
1136
+ Some clusters do not match up with the scribe environment names, so
1137
+ we figure that out here"""
1138
+ envs: List[str] = []
1139
+ for component in components:
1140
+ # If a component has a 'source_env', we use that
1141
+ # otherwise we lookup what scribe env is associated with a given cluster
1142
+ env = LOG_COMPONENTS[component].get(
1143
+ "source_env", self.cluster_to_scribe_env(cluster)
1144
+ )
1145
+ if "additional_source_envs" in LOG_COMPONENTS[component]:
1146
+ envs += LOG_COMPONENTS[component]["additional_source_envs"]
1147
+ envs.append(env)
1148
+ return set(envs)
1149
+
1150
+ def cluster_to_scribe_env(self, cluster: str) -> str:
1151
+ """Looks up the particular scribe env associated with a given paasta cluster.
1152
+
1153
+ Scribe has its own "environment" key, which doesn't always map 1:1 with our
1154
+ cluster names, so we have to maintain a manual mapping.
1155
+
1156
+ This mapping is deployed as a config file via puppet as part of the public
1157
+ config deployed to every server.
1158
+ """
1159
+ env = self.cluster_map.get(cluster, None)
1160
+ if env is None:
1161
+ print("I don't know where scribe logs for %s live?" % cluster)
1162
+ sys.exit(1)
1163
+ else:
1164
+ return env
1165
+
1166
+
1167
+ @register_log_reader("vector-logs")
1168
+ class VectorLogsReader(LogReader):
1169
+ SUPPORTS_TAILING = True
1170
+ SUPPORTS_TIME = True
1171
+
1172
+ def __init__(
1173
+ self, cluster_map: Mapping[str, Any], nats_endpoint_map: Mapping[str, Any]
1174
+ ) -> None:
1175
+ super().__init__()
1176
+
1177
+ if S3LogsReader is None:
1178
+ raise Exception("yelp_clog package must be available to use S3LogsReader")
1179
+
1180
+ self.cluster_map = cluster_map
1181
+ self.nats_endpoint_map = nats_endpoint_map
1182
+
1183
+ def get_superregion_for_cluster(self, cluster: str) -> Optional[str]:
1184
+ return self.cluster_map.get(cluster, None)
1185
+
1186
+ def get_nats_endpoint_for_cluster(self, cluster: str) -> Optional[str]:
1187
+ return self.nats_endpoint_map.get(cluster, None)
1188
+
1189
+ def print_logs_by_time(
1190
+ self,
1191
+ service,
1192
+ start_time: datetime.datetime,
1193
+ end_time: datetime.datetime,
1194
+ levels,
1195
+ components: Iterable[str],
1196
+ clusters,
1197
+ instances,
1198
+ pods,
1199
+ raw_mode,
1200
+ strip_headers,
1201
+ ) -> None:
1202
+ stream_name = get_log_name_for_service(service, prefix="app_output")
1203
+ superregion = self.get_superregion_for_cluster(clusters[0])
1204
+ reader = S3LogsReader(superregion)
1205
+ aggregated_logs: List[Dict[str, Any]] = []
1206
+
1207
+ for line in reader.get_log_reader(
1208
+ log_name=stream_name, start_datetime=start_time, end_datetime=end_time
1209
+ ):
1210
+ if paasta_app_output_passes_filter(
1211
+ line,
1212
+ levels,
1213
+ service,
1214
+ components,
1215
+ clusters,
1216
+ instances,
1217
+ pods,
1218
+ start_time=start_time,
1219
+ end_time=end_time,
1220
+ ):
1221
+ try:
1222
+ parsed_line = json.loads(line)
1223
+ timestamp = isodate.parse_datetime(parsed_line.get("timestamp"))
1224
+ if not timestamp.tzinfo:
1225
+ timestamp = pytz.utc.localize(timestamp)
1226
+ except ValueError:
1227
+ timestamp = pytz.utc.localize(datetime.datetime.min)
1228
+
1229
+ line = {"raw_line": line, "sort_key": timestamp}
1230
+ aggregated_logs.append(line)
1231
+
1232
+ aggregated_logs = list(
1233
+ {line["raw_line"]: line for line in aggregated_logs}.values()
1234
+ )
1235
+ aggregated_logs.sort(key=lambda log_line: log_line["sort_key"])
1236
+
1237
+ for line in aggregated_logs:
1238
+ print_log(line["raw_line"], levels, raw_mode, strip_headers)
1239
+
1240
+ def tail_logs(
1241
+ self,
1242
+ service: str,
1243
+ levels: Sequence[str],
1244
+ components: Iterable[str],
1245
+ clusters: Sequence[str],
1246
+ instances: List[str],
1247
+ pods: Iterable[str] = None,
1248
+ raw_mode: bool = False,
1249
+ strip_headers: bool = False,
1250
+ ) -> None:
1251
+ stream_name = get_log_name_for_service(service, prefix="app_output")
1252
+ endpoint = self.get_nats_endpoint_for_cluster(clusters[0])
1253
+ if not endpoint:
1254
+ raise NotImplementedError(
1255
+ "Tailing logs is not supported in this cluster yet, sorry"
1256
+ )
1257
+
1258
+ async def tail_logs_from_nats() -> None:
1259
+ nc = await nats.connect(f"nats://{endpoint}")
1260
+ sub = await nc.subscribe(stream_name)
1261
+
1262
+ while True:
1263
+ # Wait indefinitely for a new message (no timeout)
1264
+ msg = await sub.next_msg(timeout=None)
1265
+ decoded_data = msg.data.decode("utf-8")
1266
+
1267
+ if paasta_app_output_passes_filter(
1268
+ decoded_data,
1269
+ levels,
1270
+ service,
1271
+ components,
1272
+ clusters,
1273
+ instances,
1274
+ pods,
1275
+ ):
1276
+ await a_sync.run(
1277
+ print_log, decoded_data, levels, raw_mode, strip_headers
1278
+ )
1279
+
1280
+ a_sync.block(tail_logs_from_nats)
1281
+
1282
+
1283
+ def scribe_env_to_locations(scribe_env) -> Mapping[str, Any]:
1284
+ """Converts a scribe environment to a dictionary of locations. The
1285
+ return value is meant to be used as kwargs for `scribereader.get_tail_host_and_port`.
1286
+ """
1287
+ locations = {"ecosystem": None, "region": None, "superregion": None}
1288
+ if scribe_env in scribereader.PROD_REGIONS:
1289
+ locations["region"] = scribe_env
1290
+ elif scribe_env in scribereader.PROD_SUPERREGIONS:
1291
+ locations["superregion"] = scribe_env
1292
+ else: # non-prod envs are expressed as ecosystems
1293
+ locations["ecosystem"] = scribe_env
1294
+ return locations
1295
+
1296
+
1297
+ def generate_start_end_time(
1298
+ from_string: str = "30m", to_string: str = None
1299
+ ) -> Tuple[datetime.datetime, datetime.datetime]:
1300
+ """Parses the --from and --to command line arguments to create python
1301
+ datetime objects representing the start and end times for log retrieval
1302
+
1303
+ :param from_string: The --from argument, defaults to 30 minutes
1304
+ :param to_string: The --to argument, defaults to the time right now
1305
+ :return: A tuple containing start_time, end_time, which specify the interval of log retrieval
1306
+ """
1307
+ if to_string is None:
1308
+ end_time = datetime.datetime.utcnow()
1309
+ else:
1310
+ # Try parsing as a a natural time duration first, if that fails move on to
1311
+ # parsing as an ISO-8601 timestamp
1312
+ to_duration = timeparse(to_string)
1313
+
1314
+ if to_duration is not None:
1315
+ end_time = datetime.datetime.utcnow() - datetime.timedelta(
1316
+ seconds=to_duration
1317
+ )
1318
+ else:
1319
+ end_time = isodate.parse_datetime(to_string)
1320
+ if not end_time:
1321
+ raise ValueError(
1322
+ "--to argument not in ISO8601 format and not a valid pytimeparse duration"
1323
+ )
1324
+
1325
+ from_duration = timeparse(from_string)
1326
+ if from_duration is not None:
1327
+ start_time = datetime.datetime.utcnow() - datetime.timedelta(
1328
+ seconds=from_duration
1329
+ )
1330
+ else:
1331
+ start_time = isodate.parse_datetime(from_string)
1332
+
1333
+ if not start_time:
1334
+ raise ValueError(
1335
+ "--from argument not in ISO8601 format and not a valid pytimeparse duration"
1336
+ )
1337
+
1338
+ # Covert the timestamps to something timezone aware
1339
+ start_time = pytz.utc.localize(start_time)
1340
+ end_time = pytz.utc.localize(end_time)
1341
+
1342
+ if start_time > end_time:
1343
+ raise ValueError("Start time bigger than end time")
1344
+
1345
+ return start_time, end_time
1346
+
1347
+
1348
+ def validate_filtering_args(args: argparse.Namespace, log_reader: LogReader) -> bool:
1349
+ if not log_reader.SUPPORTS_LINE_OFFSET and args.line_offset is not None:
1350
+ print(
1351
+ PaastaColors.red(
1352
+ log_reader.__class__.__name__ + " does not support line based offsets"
1353
+ ),
1354
+ file=sys.stderr,
1355
+ )
1356
+ return False
1357
+ if not log_reader.SUPPORTS_LINE_COUNT and args.line_count is not None:
1358
+ print(
1359
+ PaastaColors.red(
1360
+ log_reader.__class__.__name__
1361
+ + " does not support line count based log retrieval"
1362
+ ),
1363
+ file=sys.stderr,
1364
+ )
1365
+ return False
1366
+ if not log_reader.SUPPORTS_TAILING and args.tail:
1367
+ print(
1368
+ PaastaColors.red(
1369
+ log_reader.__class__.__name__ + " does not support tailing"
1370
+ ),
1371
+ file=sys.stderr,
1372
+ )
1373
+ return False
1374
+ if not log_reader.SUPPORTS_TIME and (
1375
+ args.time_from is not None or args.time_to is not None
1376
+ ):
1377
+ print(
1378
+ PaastaColors.red(
1379
+ log_reader.__class__.__name__ + " does not support time based offsets"
1380
+ ),
1381
+ file=sys.stderr,
1382
+ )
1383
+ return False
1384
+
1385
+ if args.tail and (
1386
+ args.line_count is not None
1387
+ or args.time_from is not None
1388
+ or args.time_to is not None
1389
+ or args.line_offset is not None
1390
+ ):
1391
+ print(
1392
+ PaastaColors.red(
1393
+ "You cannot specify line/time based filtering parameters when tailing"
1394
+ ),
1395
+ file=sys.stderr,
1396
+ )
1397
+ return False
1398
+
1399
+ # Can't have both
1400
+ if args.line_count is not None and args.time_from is not None:
1401
+ print(
1402
+ PaastaColors.red("You cannot filter based on both line counts and time"),
1403
+ file=sys.stderr,
1404
+ )
1405
+ return False
1406
+
1407
+ return True
1408
+
1409
+
1410
+ def pick_default_log_mode(
1411
+ args: argparse.Namespace,
1412
+ log_reader: LogReader,
1413
+ service: str,
1414
+ levels: Sequence[str],
1415
+ components: Iterable[str],
1416
+ clusters: Sequence[str],
1417
+ instances: List[str],
1418
+ pods: Iterable[str],
1419
+ ) -> int:
1420
+ if log_reader.SUPPORTS_LINE_COUNT:
1421
+ print(
1422
+ PaastaColors.cyan(
1423
+ "Fetching 100 lines and applying filters. Try -n 1000 for more lines..."
1424
+ ),
1425
+ file=sys.stderr,
1426
+ )
1427
+ log_reader.print_last_n_logs(
1428
+ service=service,
1429
+ line_count=100,
1430
+ levels=levels,
1431
+ components=components,
1432
+ clusters=clusters,
1433
+ instances=instances,
1434
+ pods=pods,
1435
+ raw_mode=args.raw_mode,
1436
+ strip_headers=args.strip_headers,
1437
+ )
1438
+ return 0
1439
+ elif log_reader.SUPPORTS_TIME:
1440
+ start_time, end_time = generate_start_end_time()
1441
+ print(
1442
+ PaastaColors.cyan(
1443
+ "Fetching a specific time period and applying filters..."
1444
+ ),
1445
+ file=sys.stderr,
1446
+ )
1447
+ log_reader.print_logs_by_time(
1448
+ service=service,
1449
+ start_time=start_time,
1450
+ end_time=end_time,
1451
+ levels=levels,
1452
+ components=components,
1453
+ clusters=clusters,
1454
+ instances=instances,
1455
+ pods=pods,
1456
+ raw_mode=args.raw_mode,
1457
+ strip_headers=args.strip_headers,
1458
+ )
1459
+ return 0
1460
+ elif log_reader.SUPPORTS_TAILING:
1461
+ print(
1462
+ PaastaColors.cyan("Tailing logs and applying filters..."), file=sys.stderr
1463
+ )
1464
+ log_reader.tail_logs(
1465
+ service=service,
1466
+ levels=levels,
1467
+ components=components,
1468
+ clusters=clusters,
1469
+ instances=instances,
1470
+ pods=pods,
1471
+ raw_mode=args.raw_mode,
1472
+ strip_headers=args.strip_headers,
1473
+ )
1474
+ return 0
1475
+ return 1
1476
+
1477
+
1478
+ def paasta_logs(args: argparse.Namespace) -> int:
1479
+ """Print the logs for as Paasta service.
1480
+ :param args: argparse.Namespace obj created from sys.args by cli"""
1481
+ soa_dir = args.soa_dir
1482
+
1483
+ service = figure_out_service_name(args, soa_dir)
1484
+
1485
+ clusters = args.cluster
1486
+ if (
1487
+ args.cluster is None
1488
+ or args.instance is None
1489
+ or len(args.instance.split(",")) > 2
1490
+ ):
1491
+ print(
1492
+ PaastaColors.red("You must specify one cluster and one instance."),
1493
+ file=sys.stderr,
1494
+ )
1495
+ return 1
1496
+
1497
+ if verify_instances(args.instance, service, clusters, soa_dir):
1498
+ return 1
1499
+
1500
+ instance = args.instance
1501
+
1502
+ if args.pods is None:
1503
+ pods = None
1504
+ else:
1505
+ pods = args.pods.split(",")
1506
+
1507
+ components = args.components
1508
+ if "app_output" in args.components:
1509
+ components.remove("app_output")
1510
+ components.add("stdout")
1511
+ components.add("stderr")
1512
+
1513
+ if args.verbose:
1514
+ log.setLevel(logging.DEBUG)
1515
+ else:
1516
+ log.setLevel(logging.INFO)
1517
+
1518
+ levels = [DEFAULT_LOGLEVEL, "debug"]
1519
+
1520
+ log.debug(f"Going to get logs for {service} on cluster {clusters}")
1521
+
1522
+ log_reader = get_log_reader(components)
1523
+
1524
+ if not validate_filtering_args(args, log_reader):
1525
+ return 1
1526
+ # They haven't specified what kind of filtering they want, decide for them
1527
+ if args.line_count is None and args.time_from is None and not args.tail:
1528
+ return pick_default_log_mode(
1529
+ args, log_reader, service, levels, components, clusters, instance, pods
1530
+ )
1531
+ if args.tail:
1532
+ print(
1533
+ PaastaColors.cyan("Tailing logs and applying filters..."), file=sys.stderr
1534
+ )
1535
+ log_reader.tail_logs(
1536
+ service=service,
1537
+ levels=levels,
1538
+ components=components,
1539
+ clusters=clusters,
1540
+ instances=[instance],
1541
+ pods=pods,
1542
+ raw_mode=args.raw_mode,
1543
+ strip_headers=args.strip_headers,
1544
+ )
1545
+ return 0
1546
+
1547
+ # If the logger doesn't support offsetting the number of lines by a particular line number
1548
+ # there is no point in distinguishing between a positive/negative number of lines since it
1549
+ # can only get the last N lines
1550
+ if not log_reader.SUPPORTS_LINE_OFFSET and args.line_count is not None:
1551
+ args.line_count = abs(args.line_count)
1552
+
1553
+ # Handle line based filtering
1554
+ if args.line_count is not None and args.line_offset is None:
1555
+ log_reader.print_last_n_logs(
1556
+ service=service,
1557
+ line_count=args.line_count,
1558
+ levels=levels,
1559
+ components=components,
1560
+ clusters=clusters,
1561
+ instances=[instance],
1562
+ pods=pods,
1563
+ raw_mode=args.raw_mode,
1564
+ strip_headers=args.strip_headers,
1565
+ )
1566
+ return 0
1567
+ elif args.line_count is not None and args.line_offset is not None:
1568
+ log_reader.print_logs_by_offset(
1569
+ service=service,
1570
+ line_count=args.line_count,
1571
+ line_offset=args.line_offset,
1572
+ levels=levels,
1573
+ components=components,
1574
+ clusters=clusters,
1575
+ instances=[instance],
1576
+ pods=pods,
1577
+ raw_mode=args.raw_mode,
1578
+ strip_headers=args.strip_headers,
1579
+ )
1580
+ return 0
1581
+
1582
+ # Handle time based filtering
1583
+ try:
1584
+ start_time, end_time = generate_start_end_time(args.time_from, args.time_to)
1585
+ except ValueError as e:
1586
+ print(PaastaColors.red(str(e)), file=sys.stderr)
1587
+ return 1
1588
+
1589
+ log_reader.print_logs_by_time(
1590
+ service=service,
1591
+ start_time=start_time,
1592
+ end_time=end_time,
1593
+ levels=levels,
1594
+ components=components,
1595
+ clusters=clusters,
1596
+ instances=[instance],
1597
+ pods=pods,
1598
+ raw_mode=args.raw_mode,
1599
+ strip_headers=args.strip_headers,
1600
+ )
1601
+ return 0