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.
- k8s_itests/__init__.py +0 -0
- k8s_itests/test_autoscaling.py +23 -0
- k8s_itests/utils.py +38 -0
- paasta_tools/__init__.py +20 -0
- paasta_tools/adhoc_tools.py +142 -0
- paasta_tools/api/__init__.py +13 -0
- paasta_tools/api/api.py +330 -0
- paasta_tools/api/api_docs/swagger.json +2323 -0
- paasta_tools/api/client.py +106 -0
- paasta_tools/api/settings.py +33 -0
- paasta_tools/api/tweens/__init__.py +6 -0
- paasta_tools/api/tweens/auth.py +125 -0
- paasta_tools/api/tweens/profiling.py +108 -0
- paasta_tools/api/tweens/request_logger.py +124 -0
- paasta_tools/api/views/__init__.py +13 -0
- paasta_tools/api/views/autoscaler.py +100 -0
- paasta_tools/api/views/exception.py +45 -0
- paasta_tools/api/views/flink.py +73 -0
- paasta_tools/api/views/instance.py +395 -0
- paasta_tools/api/views/pause_autoscaler.py +71 -0
- paasta_tools/api/views/remote_run.py +113 -0
- paasta_tools/api/views/resources.py +76 -0
- paasta_tools/api/views/service.py +35 -0
- paasta_tools/api/views/version.py +25 -0
- paasta_tools/apply_external_resources.py +79 -0
- paasta_tools/async_utils.py +109 -0
- paasta_tools/autoscaling/__init__.py +0 -0
- paasta_tools/autoscaling/autoscaling_service_lib.py +57 -0
- paasta_tools/autoscaling/forecasting.py +106 -0
- paasta_tools/autoscaling/max_all_k8s_services.py +41 -0
- paasta_tools/autoscaling/pause_service_autoscaler.py +77 -0
- paasta_tools/autoscaling/utils.py +52 -0
- paasta_tools/bounce_lib.py +184 -0
- paasta_tools/broadcast_log_to_services.py +62 -0
- paasta_tools/cassandracluster_tools.py +210 -0
- paasta_tools/check_autoscaler_max_instances.py +212 -0
- paasta_tools/check_cassandracluster_services_replication.py +35 -0
- paasta_tools/check_flink_services_health.py +203 -0
- paasta_tools/check_kubernetes_api.py +57 -0
- paasta_tools/check_kubernetes_services_replication.py +141 -0
- paasta_tools/check_oom_events.py +244 -0
- paasta_tools/check_services_replication_tools.py +324 -0
- paasta_tools/check_spark_jobs.py +234 -0
- paasta_tools/cleanup_kubernetes_cr.py +138 -0
- paasta_tools/cleanup_kubernetes_crd.py +145 -0
- paasta_tools/cleanup_kubernetes_jobs.py +344 -0
- paasta_tools/cleanup_tron_namespaces.py +96 -0
- paasta_tools/cli/__init__.py +13 -0
- paasta_tools/cli/authentication.py +85 -0
- paasta_tools/cli/cli.py +260 -0
- paasta_tools/cli/cmds/__init__.py +13 -0
- paasta_tools/cli/cmds/autoscale.py +143 -0
- paasta_tools/cli/cmds/check.py +334 -0
- paasta_tools/cli/cmds/cook_image.py +147 -0
- paasta_tools/cli/cmds/get_docker_image.py +76 -0
- paasta_tools/cli/cmds/get_image_version.py +172 -0
- paasta_tools/cli/cmds/get_latest_deployment.py +93 -0
- paasta_tools/cli/cmds/info.py +155 -0
- paasta_tools/cli/cmds/itest.py +117 -0
- paasta_tools/cli/cmds/list.py +66 -0
- paasta_tools/cli/cmds/list_clusters.py +42 -0
- paasta_tools/cli/cmds/list_deploy_queue.py +171 -0
- paasta_tools/cli/cmds/list_namespaces.py +84 -0
- paasta_tools/cli/cmds/local_run.py +1396 -0
- paasta_tools/cli/cmds/logs.py +1601 -0
- paasta_tools/cli/cmds/mark_for_deployment.py +1988 -0
- paasta_tools/cli/cmds/mesh_status.py +174 -0
- paasta_tools/cli/cmds/pause_service_autoscaler.py +107 -0
- paasta_tools/cli/cmds/push_to_registry.py +275 -0
- paasta_tools/cli/cmds/remote_run.py +252 -0
- paasta_tools/cli/cmds/rollback.py +347 -0
- paasta_tools/cli/cmds/secret.py +549 -0
- paasta_tools/cli/cmds/security_check.py +59 -0
- paasta_tools/cli/cmds/spark_run.py +1400 -0
- paasta_tools/cli/cmds/start_stop_restart.py +401 -0
- paasta_tools/cli/cmds/status.py +2302 -0
- paasta_tools/cli/cmds/validate.py +1012 -0
- paasta_tools/cli/cmds/wait_for_deployment.py +275 -0
- paasta_tools/cli/fsm/__init__.py +13 -0
- paasta_tools/cli/fsm/autosuggest.py +82 -0
- paasta_tools/cli/fsm/template/README.md +8 -0
- paasta_tools/cli/fsm/template/cookiecutter.json +7 -0
- paasta_tools/cli/fsm/template/{{cookiecutter.service}}/kubernetes-PROD.yaml +91 -0
- paasta_tools/cli/fsm/template/{{cookiecutter.service}}/monitoring.yaml +20 -0
- paasta_tools/cli/fsm/template/{{cookiecutter.service}}/service.yaml +8 -0
- paasta_tools/cli/fsm/template/{{cookiecutter.service}}/smartstack.yaml +6 -0
- paasta_tools/cli/fsm_cmd.py +121 -0
- paasta_tools/cli/paasta_tabcomplete.sh +23 -0
- paasta_tools/cli/schemas/adhoc_schema.json +199 -0
- paasta_tools/cli/schemas/autoscaling_schema.json +91 -0
- paasta_tools/cli/schemas/autotuned_defaults/cassandracluster_schema.json +37 -0
- paasta_tools/cli/schemas/autotuned_defaults/kubernetes_schema.json +89 -0
- paasta_tools/cli/schemas/deploy_schema.json +173 -0
- paasta_tools/cli/schemas/eks_schema.json +970 -0
- paasta_tools/cli/schemas/kubernetes_schema.json +970 -0
- paasta_tools/cli/schemas/rollback_schema.json +160 -0
- paasta_tools/cli/schemas/service_schema.json +25 -0
- paasta_tools/cli/schemas/smartstack_schema.json +322 -0
- paasta_tools/cli/schemas/tron_schema.json +699 -0
- paasta_tools/cli/utils.py +1118 -0
- paasta_tools/clusterman.py +21 -0
- paasta_tools/config_utils.py +385 -0
- paasta_tools/contrib/__init__.py +0 -0
- paasta_tools/contrib/bounce_log_latency_parser.py +68 -0
- paasta_tools/contrib/check_manual_oapi_changes.sh +24 -0
- paasta_tools/contrib/check_orphans.py +306 -0
- paasta_tools/contrib/create_dynamodb_table.py +35 -0
- paasta_tools/contrib/create_paasta_playground.py +105 -0
- paasta_tools/contrib/emit_allocated_cpu_metrics.py +50 -0
- paasta_tools/contrib/get_running_task_allocation.py +346 -0
- paasta_tools/contrib/habitat_fixer.py +86 -0
- paasta_tools/contrib/ide_helper.py +316 -0
- paasta_tools/contrib/is_pod_healthy_in_proxy.py +139 -0
- paasta_tools/contrib/is_pod_healthy_in_smartstack.py +50 -0
- paasta_tools/contrib/kill_bad_containers.py +109 -0
- paasta_tools/contrib/mass-deploy-tag.sh +44 -0
- paasta_tools/contrib/mock_patch_checker.py +86 -0
- paasta_tools/contrib/paasta_update_soa_memcpu.py +520 -0
- paasta_tools/contrib/render_template.py +129 -0
- paasta_tools/contrib/rightsizer_soaconfigs_update.py +348 -0
- paasta_tools/contrib/service_shard_remove.py +157 -0
- paasta_tools/contrib/service_shard_update.py +373 -0
- paasta_tools/contrib/shared_ip_check.py +77 -0
- paasta_tools/contrib/timeouts_metrics_prom.py +64 -0
- paasta_tools/delete_kubernetes_deployments.py +89 -0
- paasta_tools/deployment_utils.py +44 -0
- paasta_tools/docker_wrapper.py +234 -0
- paasta_tools/docker_wrapper_imports.py +13 -0
- paasta_tools/drain_lib.py +351 -0
- paasta_tools/dump_locally_running_services.py +71 -0
- paasta_tools/eks_tools.py +119 -0
- paasta_tools/envoy_tools.py +373 -0
- paasta_tools/firewall.py +504 -0
- paasta_tools/firewall_logging.py +154 -0
- paasta_tools/firewall_update.py +172 -0
- paasta_tools/flink_tools.py +345 -0
- paasta_tools/flinkeks_tools.py +90 -0
- paasta_tools/frameworks/__init__.py +0 -0
- paasta_tools/frameworks/adhoc_scheduler.py +71 -0
- paasta_tools/frameworks/constraints.py +87 -0
- paasta_tools/frameworks/native_scheduler.py +652 -0
- paasta_tools/frameworks/native_service_config.py +301 -0
- paasta_tools/frameworks/task_store.py +245 -0
- paasta_tools/generate_all_deployments +9 -0
- paasta_tools/generate_authenticating_services.py +94 -0
- paasta_tools/generate_deployments_for_service.py +255 -0
- paasta_tools/generate_services_file.py +114 -0
- paasta_tools/generate_services_yaml.py +30 -0
- paasta_tools/hacheck.py +76 -0
- paasta_tools/instance/__init__.py +0 -0
- paasta_tools/instance/hpa_metrics_parser.py +122 -0
- paasta_tools/instance/kubernetes.py +1362 -0
- paasta_tools/iptables.py +240 -0
- paasta_tools/kafkacluster_tools.py +143 -0
- paasta_tools/kubernetes/__init__.py +0 -0
- paasta_tools/kubernetes/application/__init__.py +0 -0
- paasta_tools/kubernetes/application/controller_wrappers.py +476 -0
- paasta_tools/kubernetes/application/tools.py +90 -0
- paasta_tools/kubernetes/bin/__init__.py +0 -0
- paasta_tools/kubernetes/bin/kubernetes_remove_evicted_pods.py +164 -0
- paasta_tools/kubernetes/bin/paasta_cleanup_remote_run_resources.py +135 -0
- paasta_tools/kubernetes/bin/paasta_cleanup_stale_nodes.py +181 -0
- paasta_tools/kubernetes/bin/paasta_secrets_sync.py +758 -0
- paasta_tools/kubernetes/remote_run.py +558 -0
- paasta_tools/kubernetes_tools.py +4679 -0
- paasta_tools/list_kubernetes_service_instances.py +128 -0
- paasta_tools/list_tron_namespaces.py +60 -0
- paasta_tools/long_running_service_tools.py +678 -0
- paasta_tools/mac_address.py +44 -0
- paasta_tools/marathon_dashboard.py +0 -0
- paasta_tools/mesos/__init__.py +0 -0
- paasta_tools/mesos/cfg.py +46 -0
- paasta_tools/mesos/cluster.py +60 -0
- paasta_tools/mesos/exceptions.py +59 -0
- paasta_tools/mesos/framework.py +77 -0
- paasta_tools/mesos/log.py +48 -0
- paasta_tools/mesos/master.py +306 -0
- paasta_tools/mesos/mesos_file.py +169 -0
- paasta_tools/mesos/parallel.py +52 -0
- paasta_tools/mesos/slave.py +115 -0
- paasta_tools/mesos/task.py +94 -0
- paasta_tools/mesos/util.py +69 -0
- paasta_tools/mesos/zookeeper.py +37 -0
- paasta_tools/mesos_maintenance.py +848 -0
- paasta_tools/mesos_tools.py +1051 -0
- paasta_tools/metrics/__init__.py +0 -0
- paasta_tools/metrics/metastatus_lib.py +1110 -0
- paasta_tools/metrics/metrics_lib.py +217 -0
- paasta_tools/monitoring/__init__.py +13 -0
- paasta_tools/monitoring/check_k8s_api_performance.py +110 -0
- paasta_tools/monitoring_tools.py +652 -0
- paasta_tools/monkrelaycluster_tools.py +146 -0
- paasta_tools/nrtsearchservice_tools.py +143 -0
- paasta_tools/nrtsearchserviceeks_tools.py +68 -0
- paasta_tools/oom_logger.py +321 -0
- paasta_tools/paasta_deploy_tron_jobs +3 -0
- paasta_tools/paasta_execute_docker_command.py +123 -0
- paasta_tools/paasta_native_serviceinit.py +21 -0
- paasta_tools/paasta_service_config_loader.py +201 -0
- paasta_tools/paastaapi/__init__.py +29 -0
- paasta_tools/paastaapi/api/__init__.py +3 -0
- paasta_tools/paastaapi/api/autoscaler_api.py +302 -0
- paasta_tools/paastaapi/api/default_api.py +569 -0
- paasta_tools/paastaapi/api/remote_run_api.py +604 -0
- paasta_tools/paastaapi/api/resources_api.py +157 -0
- paasta_tools/paastaapi/api/service_api.py +1736 -0
- paasta_tools/paastaapi/api_client.py +818 -0
- paasta_tools/paastaapi/apis/__init__.py +22 -0
- paasta_tools/paastaapi/configuration.py +455 -0
- paasta_tools/paastaapi/exceptions.py +137 -0
- paasta_tools/paastaapi/model/__init__.py +5 -0
- paasta_tools/paastaapi/model/adhoc_launch_history.py +176 -0
- paasta_tools/paastaapi/model/autoscaler_count_msg.py +176 -0
- paasta_tools/paastaapi/model/deploy_queue.py +178 -0
- paasta_tools/paastaapi/model/deploy_queue_service_instance.py +194 -0
- paasta_tools/paastaapi/model/envoy_backend.py +185 -0
- paasta_tools/paastaapi/model/envoy_location.py +184 -0
- paasta_tools/paastaapi/model/envoy_status.py +181 -0
- paasta_tools/paastaapi/model/flink_cluster_overview.py +188 -0
- paasta_tools/paastaapi/model/flink_config.py +173 -0
- paasta_tools/paastaapi/model/flink_job.py +186 -0
- paasta_tools/paastaapi/model/flink_job_details.py +192 -0
- paasta_tools/paastaapi/model/flink_jobs.py +175 -0
- paasta_tools/paastaapi/model/float_and_error.py +173 -0
- paasta_tools/paastaapi/model/hpa_metric.py +176 -0
- paasta_tools/paastaapi/model/inline_object.py +170 -0
- paasta_tools/paastaapi/model/inline_response200.py +170 -0
- paasta_tools/paastaapi/model/inline_response2001.py +170 -0
- paasta_tools/paastaapi/model/instance_bounce_status.py +200 -0
- paasta_tools/paastaapi/model/instance_mesh_status.py +186 -0
- paasta_tools/paastaapi/model/instance_status.py +220 -0
- paasta_tools/paastaapi/model/instance_status_adhoc.py +187 -0
- paasta_tools/paastaapi/model/instance_status_cassandracluster.py +173 -0
- paasta_tools/paastaapi/model/instance_status_flink.py +173 -0
- paasta_tools/paastaapi/model/instance_status_kafkacluster.py +173 -0
- paasta_tools/paastaapi/model/instance_status_kubernetes.py +263 -0
- paasta_tools/paastaapi/model/instance_status_kubernetes_autoscaling_status.py +187 -0
- paasta_tools/paastaapi/model/instance_status_kubernetes_v2.py +197 -0
- paasta_tools/paastaapi/model/instance_status_tron.py +204 -0
- paasta_tools/paastaapi/model/instance_tasks.py +182 -0
- paasta_tools/paastaapi/model/integer_and_error.py +173 -0
- paasta_tools/paastaapi/model/kubernetes_container.py +178 -0
- paasta_tools/paastaapi/model/kubernetes_container_v2.py +219 -0
- paasta_tools/paastaapi/model/kubernetes_healthcheck.py +176 -0
- paasta_tools/paastaapi/model/kubernetes_pod.py +201 -0
- paasta_tools/paastaapi/model/kubernetes_pod_event.py +176 -0
- paasta_tools/paastaapi/model/kubernetes_pod_v2.py +213 -0
- paasta_tools/paastaapi/model/kubernetes_replica_set.py +185 -0
- paasta_tools/paastaapi/model/kubernetes_version.py +202 -0
- paasta_tools/paastaapi/model/remote_run_outcome.py +189 -0
- paasta_tools/paastaapi/model/remote_run_start.py +185 -0
- paasta_tools/paastaapi/model/remote_run_stop.py +176 -0
- paasta_tools/paastaapi/model/remote_run_token.py +173 -0
- paasta_tools/paastaapi/model/resource.py +187 -0
- paasta_tools/paastaapi/model/resource_item.py +187 -0
- paasta_tools/paastaapi/model/resource_value.py +176 -0
- paasta_tools/paastaapi/model/smartstack_backend.py +191 -0
- paasta_tools/paastaapi/model/smartstack_location.py +181 -0
- paasta_tools/paastaapi/model/smartstack_status.py +181 -0
- paasta_tools/paastaapi/model/task_tail_lines.py +176 -0
- paasta_tools/paastaapi/model_utils.py +1879 -0
- paasta_tools/paastaapi/models/__init__.py +62 -0
- paasta_tools/paastaapi/rest.py +287 -0
- paasta_tools/prune_completed_pods.py +220 -0
- paasta_tools/puppet_service_tools.py +59 -0
- paasta_tools/py.typed +1 -0
- paasta_tools/remote_git.py +127 -0
- paasta_tools/run-paasta-api-in-dev-mode.py +57 -0
- paasta_tools/run-paasta-api-playground.py +51 -0
- paasta_tools/secret_providers/__init__.py +66 -0
- paasta_tools/secret_providers/vault.py +214 -0
- paasta_tools/secret_tools.py +277 -0
- paasta_tools/setup_istio_mesh.py +353 -0
- paasta_tools/setup_kubernetes_cr.py +412 -0
- paasta_tools/setup_kubernetes_crd.py +138 -0
- paasta_tools/setup_kubernetes_internal_crd.py +154 -0
- paasta_tools/setup_kubernetes_job.py +353 -0
- paasta_tools/setup_prometheus_adapter_config.py +1028 -0
- paasta_tools/setup_tron_namespace.py +248 -0
- paasta_tools/slack.py +75 -0
- paasta_tools/smartstack_tools.py +676 -0
- paasta_tools/spark_tools.py +283 -0
- paasta_tools/synapse_srv_namespaces_fact.py +42 -0
- paasta_tools/tron/__init__.py +0 -0
- paasta_tools/tron/client.py +158 -0
- paasta_tools/tron/tron_command_context.py +194 -0
- paasta_tools/tron/tron_timeutils.py +101 -0
- paasta_tools/tron_tools.py +1448 -0
- paasta_tools/utils.py +4307 -0
- paasta_tools/yaml_tools.py +44 -0
- paasta_tools-1.21.3.data/scripts/apply_external_resources.py +79 -0
- paasta_tools-1.21.3.data/scripts/bounce_log_latency_parser.py +68 -0
- paasta_tools-1.21.3.data/scripts/check_autoscaler_max_instances.py +212 -0
- paasta_tools-1.21.3.data/scripts/check_cassandracluster_services_replication.py +35 -0
- paasta_tools-1.21.3.data/scripts/check_flink_services_health.py +203 -0
- paasta_tools-1.21.3.data/scripts/check_kubernetes_api.py +57 -0
- paasta_tools-1.21.3.data/scripts/check_kubernetes_services_replication.py +141 -0
- paasta_tools-1.21.3.data/scripts/check_manual_oapi_changes.sh +24 -0
- paasta_tools-1.21.3.data/scripts/check_oom_events.py +244 -0
- paasta_tools-1.21.3.data/scripts/check_orphans.py +306 -0
- paasta_tools-1.21.3.data/scripts/check_spark_jobs.py +234 -0
- paasta_tools-1.21.3.data/scripts/cleanup_kubernetes_cr.py +138 -0
- paasta_tools-1.21.3.data/scripts/cleanup_kubernetes_crd.py +145 -0
- paasta_tools-1.21.3.data/scripts/cleanup_kubernetes_jobs.py +344 -0
- paasta_tools-1.21.3.data/scripts/create_dynamodb_table.py +35 -0
- paasta_tools-1.21.3.data/scripts/create_paasta_playground.py +105 -0
- paasta_tools-1.21.3.data/scripts/delete_kubernetes_deployments.py +89 -0
- paasta_tools-1.21.3.data/scripts/emit_allocated_cpu_metrics.py +50 -0
- paasta_tools-1.21.3.data/scripts/generate_all_deployments +9 -0
- paasta_tools-1.21.3.data/scripts/generate_authenticating_services.py +94 -0
- paasta_tools-1.21.3.data/scripts/generate_deployments_for_service.py +255 -0
- paasta_tools-1.21.3.data/scripts/generate_services_file.py +114 -0
- paasta_tools-1.21.3.data/scripts/generate_services_yaml.py +30 -0
- paasta_tools-1.21.3.data/scripts/get_running_task_allocation.py +346 -0
- paasta_tools-1.21.3.data/scripts/habitat_fixer.py +86 -0
- paasta_tools-1.21.3.data/scripts/ide_helper.py +316 -0
- paasta_tools-1.21.3.data/scripts/is_pod_healthy_in_proxy.py +139 -0
- paasta_tools-1.21.3.data/scripts/is_pod_healthy_in_smartstack.py +50 -0
- paasta_tools-1.21.3.data/scripts/kill_bad_containers.py +109 -0
- paasta_tools-1.21.3.data/scripts/kubernetes_remove_evicted_pods.py +164 -0
- paasta_tools-1.21.3.data/scripts/mass-deploy-tag.sh +44 -0
- paasta_tools-1.21.3.data/scripts/mock_patch_checker.py +86 -0
- paasta_tools-1.21.3.data/scripts/paasta_cleanup_remote_run_resources.py +135 -0
- paasta_tools-1.21.3.data/scripts/paasta_cleanup_stale_nodes.py +181 -0
- paasta_tools-1.21.3.data/scripts/paasta_deploy_tron_jobs +3 -0
- paasta_tools-1.21.3.data/scripts/paasta_execute_docker_command.py +123 -0
- paasta_tools-1.21.3.data/scripts/paasta_secrets_sync.py +758 -0
- paasta_tools-1.21.3.data/scripts/paasta_tabcomplete.sh +23 -0
- paasta_tools-1.21.3.data/scripts/paasta_update_soa_memcpu.py +520 -0
- paasta_tools-1.21.3.data/scripts/render_template.py +129 -0
- paasta_tools-1.21.3.data/scripts/rightsizer_soaconfigs_update.py +348 -0
- paasta_tools-1.21.3.data/scripts/service_shard_remove.py +157 -0
- paasta_tools-1.21.3.data/scripts/service_shard_update.py +373 -0
- paasta_tools-1.21.3.data/scripts/setup_istio_mesh.py +353 -0
- paasta_tools-1.21.3.data/scripts/setup_kubernetes_cr.py +412 -0
- paasta_tools-1.21.3.data/scripts/setup_kubernetes_crd.py +138 -0
- paasta_tools-1.21.3.data/scripts/setup_kubernetes_internal_crd.py +154 -0
- paasta_tools-1.21.3.data/scripts/setup_kubernetes_job.py +353 -0
- paasta_tools-1.21.3.data/scripts/setup_prometheus_adapter_config.py +1028 -0
- paasta_tools-1.21.3.data/scripts/shared_ip_check.py +77 -0
- paasta_tools-1.21.3.data/scripts/synapse_srv_namespaces_fact.py +42 -0
- paasta_tools-1.21.3.data/scripts/timeouts_metrics_prom.py +64 -0
- paasta_tools-1.21.3.dist-info/LICENSE +201 -0
- paasta_tools-1.21.3.dist-info/METADATA +74 -0
- paasta_tools-1.21.3.dist-info/RECORD +348 -0
- paasta_tools-1.21.3.dist-info/WHEEL +5 -0
- paasta_tools-1.21.3.dist-info/entry_points.txt +20 -0
- 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
|