From dff2853d29b524f455acb0ee3aebdb3616d7f4ac Mon Sep 17 00:00:00 2001 From: Vahid Hashemian Date: Sat, 6 Feb 2021 02:38:59 +0000 Subject: [PATCH] Rename the project to DoctorK Due to current name violating Apache policies. --- README.md | 66 ++++++------ docs/APIs.md | 18 ++-- docs/DESIGN.md | 18 ++-- docs/doctork_diagram.png | Bin 0 -> 27312 bytes docs/doctork_logo.svg | 10 ++ docs/doctork_ui.png | Bin 0 -> 47829 bytes docs/doctorkafka_diagram.png | Bin 27566 -> 0 bytes docs/doctorkafka_logo.svg | 16 --- docs/doctorkafka_ui.png | Bin 50491 -> 0 bytes {drkafka => doctork}/Dockerfile | 6 +- .../config/doctork.properties | 94 ++++++++-------- {drkafka => doctork}/config/log4j2.dev.xml | 4 +- {drkafka => doctork}/config/log4j2.xml | 4 +- {drkafka => doctork}/pom.xml | 10 +- doctork/scripts/action_retriever.sh | 8 ++ {drkafka => doctork}/scripts/run.sh | 2 +- doctork/scripts/run_dev.sh | 7 ++ .../scripts/run_in_container.sh | 6 +- doctork/scripts/run_prod.sh | 8 ++ .../src/main/assembly/doctork.xml | 0 .../java/com/pinterest/doctork/DoctorK.java | 60 +++++------ .../doctork/DoctorKActionReporter.java | 12 +-- .../pinterest/doctork/DoctorKHeartbeat.java | 10 +- .../com/pinterest/doctork/DoctorKMain.java | 102 +++++++++--------- .../com/pinterest/doctork/DoctorKMetrics.java | 9 ++ .../com/pinterest/doctork/DoctorKWatcher.java | 8 +- .../com/pinterest/doctork}/KafkaBroker.java | 8 +- .../com/pinterest/doctork}/KafkaCluster.java | 10 +- .../doctork}/KafkaClusterManager.java | 72 ++++++------- .../pinterest/doctork}/api/BrokersApi.java | 14 +-- .../doctork}/api/BrokersDecommissionApi.java | 18 ++-- .../pinterest/doctork}/api/ClustersApi.java | 12 +-- .../doctork}/api/ClustersMaintenanceApi.java | 20 ++-- .../com/pinterest/doctork/api/DoctorKApi.java | 26 ++--- .../doctork/config/DoctorKAppConfig.java | 6 +- .../doctork/config/DoctorKClusterConfig.java | 8 +- .../doctork/config/DoctorKConfig.java | 87 ++++++++------- .../doctork}/errors/ClusterInfoError.java | 4 +- .../doctork}/notification/Email.java | 8 +- .../replicastats/BrokerStatsProcessor.java | 15 ++- .../PastReplicaStatsProcessor.java | 16 +-- .../replicastats/ReplicaStatsManager.java | 18 ++-- .../security/DoctorKAuthorizationFilter.java | 8 +- .../security/DoctorKSecurityContext.java | 12 +-- .../security/SampleAuthorizationFilter.java | 24 ++--- .../doctork}/security/UserPrincipal.java | 2 +- .../doctork}/servlet/ClusterInfoServlet.java | 18 ++-- .../servlet/DoctorKActionsServlet.java | 28 ++--- .../servlet/DoctorKBrokerStatsServlet.java | 26 ++--- .../doctork/servlet/DoctorKInfoServlet.java | 14 +-- .../doctork/servlet/DoctorKServlet.java | 12 +-- .../doctork/servlet/DoctorKWebServer.java | 12 +-- .../servlet/KafkaTopicStatsServlet.java | 18 ++-- .../UnderReplicatedPartitionsServlet.java | 14 +-- .../doctork}/tools/BrokerReplacement.java | 4 +- .../doctork}/tools/BrokerStatsFilter.java | 10 +- .../doctork}/tools/ClusterLoadBalancer.java | 20 ++-- .../doctork/tools/DoctorKActionRetriever.java | 14 +-- .../doctork/tools/DoctorKActionWriter.java | 10 +- .../doctork/tools/DoctorKZookeeperClient.java | 10 +- .../pinterest/doctork}/tools/KafkaWriter.java | 4 +- .../doctork}/tools/ReplicaStatsRetriever.java | 12 +-- .../pinterest/doctork}/tools/URPChecker.java | 6 +- .../com/pinterest/doctork}/util/ApiUtils.java | 2 +- .../doctork}/util/BrokerReplacer.java | 4 +- .../doctork}/util/OutOfSyncReplica.java | 2 +- .../util/PreferredReplicaElectionInfo.java | 2 +- .../doctork}/util/ReassignmentInfo.java | 4 +- .../doctork}/util/ReplicaStatsUtil.java | 2 +- .../doctork}/util/UnderReplicatedReason.java | 2 +- .../doctork}/util/ZookeeperClient.java | 10 +- .../src/main/resources/log4j.properties | 0 .../src/main/resources/versioning.properties | 2 + .../resources/webapp/pages/clusterinfo.html | 8 +- .../main/resources/webapp/pages/index.html | 14 +-- .../doctork}/ClusterInfoServletTest.java | 12 +-- .../pinterest/doctork}/KafkaBrokerTest.java | 10 +- .../doctork}/KafkaClusterManagerTest.java | 8 +- .../pinterest/doctork}/KafkaClusterTest.java | 66 ++++++------ .../doctork}/KafkaOperatorConfigTest.java | 10 +- {drkafka => doctork}/teletraan/RESTARTING | 0 .../config/cron/ostrich_metrics_to_tsd | 0 .../teletraan/config/serviceset.conf | 8 +- drkafka/scripts/action_retriever.sh | 8 -- drkafka/scripts/run_dev.sh | 7 -- drkafka/scripts/run_prod.sh | 8 -- .../doctorkafka/DoctorKafkaMetrics.java | 9 -- .../src/main/resources/versioning.properties | 2 - kafkastats/deb.version | 2 +- kafkastats/kafkastats.upstart | 2 +- kafkastats/pom.xml | 6 +- kafkastats/scripts/brokerStats.sh | 2 +- kafkastats/scripts/metricsCollector.sh | 4 +- kafkastats/scripts/metricsFetcher.sh | 4 +- kafkastats/scripts/metricsLocalhost.sh | 4 +- kafkastats/scripts/statscollector.upstart | 2 +- kafkastats/src/main/avro/brokerstats.avdl | 2 +- .../stats/BrokerStatsReporter.java | 4 +- .../stats/BrokerStatsRetriever.java | 18 ++-- .../stats/KafkaAvroPublisher.java | 8 +- .../stats/KafkaMetricRetrievingTask.java | 2 +- .../stats/KafkaMetricValue.java | 2 +- .../stats/KafkaStatsMain.java | 4 +- .../stats/MetricsRetriever.java | 4 +- .../stats/ReplicaStatsTask.java | 4 +- .../tools/BrokerStatsReader.java | 8 +- .../tools/MetricsFetcher.java | 2 +- .../util/KafkaUtils.java | 4 +- .../util/MetricsPusher.java | 2 +- .../util/OpenTsdbClient.java | 2 +- .../util/OpenTsdbMetricConverter.java | 2 +- .../util/OperatorUtil.java | 7 +- .../util/OstrichAdminService.java | 2 +- pom.xml | 10 +- 114 files changed, 700 insertions(+), 711 deletions(-) create mode 100644 docs/doctork_diagram.png create mode 100644 docs/doctork_logo.svg create mode 100644 docs/doctork_ui.png delete mode 100644 docs/doctorkafka_diagram.png delete mode 100644 docs/doctorkafka_logo.svg delete mode 100644 docs/doctorkafka_ui.png rename {drkafka => doctork}/Dockerfile (61%) rename drkafka/config/doctorkafka.properties => doctork/config/doctork.properties (60%) rename {drkafka => doctork}/config/log4j2.dev.xml (77%) rename {drkafka => doctork}/config/log4j2.xml (75%) rename {drkafka => doctork}/pom.xml (97%) create mode 100755 doctork/scripts/action_retriever.sh rename {drkafka => doctork}/scripts/run.sh (62%) create mode 100755 doctork/scripts/run_dev.sh rename {drkafka => doctork}/scripts/run_in_container.sh (87%) create mode 100755 doctork/scripts/run_prod.sh rename drkafka/src/main/assembly/doctorkafka.xml => doctork/src/main/assembly/doctork.xml (100%) rename drkafka/src/main/java/com/pinterest/doctorkafka/DoctorKafka.java => doctork/src/main/java/com/pinterest/doctork/DoctorK.java (57%) rename drkafka/src/main/java/com/pinterest/doctorkafka/DoctorKafkaActionReporter.java => doctork/src/main/java/com/pinterest/doctork/DoctorKActionReporter.java (91%) rename drkafka/src/main/java/com/pinterest/doctorkafka/DoctorKafkaHeartbeat.java => doctork/src/main/java/com/pinterest/doctork/DoctorKHeartbeat.java (72%) rename drkafka/src/main/java/com/pinterest/doctorkafka/DoctorKafkaMain.java => doctork/src/main/java/com/pinterest/doctork/DoctorKMain.java (60%) create mode 100644 doctork/src/main/java/com/pinterest/doctork/DoctorKMetrics.java rename drkafka/src/main/java/com/pinterest/doctorkafka/DoctorKafkaWatcher.java => doctork/src/main/java/com/pinterest/doctork/DoctorKWatcher.java (87%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/KafkaBroker.java (97%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/KafkaCluster.java (98%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/KafkaClusterManager.java (95%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/api/BrokersApi.java (65%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/api/BrokersDecommissionApi.java (80%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/api/ClustersApi.java (57%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/api/ClustersMaintenanceApi.java (76%) rename drkafka/src/main/java/com/pinterest/doctorkafka/api/DoctorKafkaApi.java => doctork/src/main/java/com/pinterest/doctork/api/DoctorKApi.java (58%) rename drkafka/src/main/java/com/pinterest/doctorkafka/config/DoctorKafkaAppConfig.java => doctork/src/main/java/com/pinterest/doctork/config/DoctorKAppConfig.java (75%) rename drkafka/src/main/java/com/pinterest/doctorkafka/config/DoctorKafkaClusterConfig.java => doctork/src/main/java/com/pinterest/doctork/config/DoctorKClusterConfig.java (95%) rename drkafka/src/main/java/com/pinterest/doctorkafka/config/DoctorKafkaConfig.java => doctork/src/main/java/com/pinterest/doctork/config/DoctorKConfig.java (72%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/errors/ClusterInfoError.java (81%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/notification/Email.java (97%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/replicastats/BrokerStatsProcessor.java (86%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/replicastats/PastReplicaStatsProcessor.java (87%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/replicastats/ReplicaStatsManager.java (88%) rename drkafka/src/main/java/com/pinterest/doctorkafka/security/DrKafkaAuthorizationFilter.java => doctork/src/main/java/com/pinterest/doctork/security/DoctorKAuthorizationFilter.java (58%) rename drkafka/src/main/java/com/pinterest/doctorkafka/security/DrKafkaSecurityContext.java => doctork/src/main/java/com/pinterest/doctork/security/DoctorKSecurityContext.java (65%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/security/SampleAuthorizationFilter.java (73%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/security/UserPrincipal.java (89%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/servlet/ClusterInfoServlet.java (91%) rename drkafka/src/main/java/com/pinterest/doctorkafka/servlet/DoctorKafkaActionsServlet.java => doctork/src/main/java/com/pinterest/doctork/servlet/DoctorKActionsServlet.java (85%) rename drkafka/src/main/java/com/pinterest/doctorkafka/servlet/DoctorKafkaBrokerStatsServlet.java => doctork/src/main/java/com/pinterest/doctork/servlet/DoctorKBrokerStatsServlet.java (88%) rename drkafka/src/main/java/com/pinterest/doctorkafka/servlet/DoctorKafkaInfoServlet.java => doctork/src/main/java/com/pinterest/doctork/servlet/DoctorKInfoServlet.java (87%) rename drkafka/src/main/java/com/pinterest/doctorkafka/servlet/DoctorKafkaServlet.java => doctork/src/main/java/com/pinterest/doctork/servlet/DoctorKServlet.java (94%) rename drkafka/src/main/java/com/pinterest/doctorkafka/servlet/DoctorKafkaWebServer.java => doctork/src/main/java/com/pinterest/doctork/servlet/DoctorKWebServer.java (88%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/servlet/KafkaTopicStatsServlet.java (89%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/servlet/UnderReplicatedPartitionsServlet.java (81%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/tools/BrokerReplacement.java (95%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/tools/BrokerStatsFilter.java (96%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/tools/ClusterLoadBalancer.java (88%) rename drkafka/src/main/java/com/pinterest/doctorkafka/tools/DoctorKafkaActionRetriever.java => doctork/src/main/java/com/pinterest/doctork/tools/DoctorKActionRetriever.java (92%) rename drkafka/src/main/java/com/pinterest/doctorkafka/tools/DoctorKafkaActionWriter.java => doctork/src/main/java/com/pinterest/doctork/tools/DoctorKActionWriter.java (87%) rename drkafka/src/main/java/com/pinterest/doctorkafka/tools/DoctorKafkaZookeeperClient.java => doctork/src/main/java/com/pinterest/doctork/tools/DoctorKZookeeperClient.java (90%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/tools/KafkaWriter.java (97%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/tools/ReplicaStatsRetriever.java (93%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/tools/URPChecker.java (95%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/util/ApiUtils.java (88%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/util/BrokerReplacer.java (95%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/util/OutOfSyncReplica.java (98%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/util/PreferredReplicaElectionInfo.java (91%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/util/ReassignmentInfo.java (86%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/util/ReplicaStatsUtil.java (96%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/util/UnderReplicatedReason.java (82%) rename {drkafka/src/main/java/com/pinterest/doctorkafka => doctork/src/main/java/com/pinterest/doctork}/util/ZookeeperClient.java (94%) rename {drkafka => doctork}/src/main/resources/log4j.properties (100%) create mode 100644 doctork/src/main/resources/versioning.properties rename {drkafka => doctork}/src/main/resources/webapp/pages/clusterinfo.html (91%) rename {drkafka => doctork}/src/main/resources/webapp/pages/index.html (88%) rename {drkafka/src/test/java/com/pinterest/doctorkafka => doctork/src/test/java/com/pinterest/doctork}/ClusterInfoServletTest.java (80%) rename {drkafka/src/test/java/com/pinterest/doctorkafka => doctork/src/test/java/com/pinterest/doctork}/KafkaBrokerTest.java (63%) rename {drkafka/src/test/java/com/pinterest/doctorkafka => doctork/src/test/java/com/pinterest/doctork}/KafkaClusterManagerTest.java (86%) rename {drkafka/src/test/java/com/pinterest/doctorkafka => doctork/src/test/java/com/pinterest/doctork}/KafkaClusterTest.java (86%) rename {drkafka/src/test/java/com/pinterest/doctorkafka => doctork/src/test/java/com/pinterest/doctork}/KafkaOperatorConfigTest.java (66%) rename {drkafka => doctork}/teletraan/RESTARTING (100%) rename {drkafka => doctork}/teletraan/config/cron/ostrich_metrics_to_tsd (100%) rename {drkafka => doctork}/teletraan/config/serviceset.conf (80%) delete mode 100755 drkafka/scripts/action_retriever.sh delete mode 100755 drkafka/scripts/run_dev.sh delete mode 100755 drkafka/scripts/run_prod.sh delete mode 100644 drkafka/src/main/java/com/pinterest/doctorkafka/DoctorKafkaMetrics.java delete mode 100644 drkafka/src/main/resources/versioning.properties rename kafkastats/src/main/java/com/pinterest/{doctorkafka => doctork}/stats/BrokerStatsReporter.java (96%) rename kafkastats/src/main/java/com/pinterest/{doctorkafka => doctork}/stats/BrokerStatsRetriever.java (98%) rename kafkastats/src/main/java/com/pinterest/{doctorkafka => doctork}/stats/KafkaAvroPublisher.java (94%) rename kafkastats/src/main/java/com/pinterest/{doctorkafka => doctork}/stats/KafkaMetricRetrievingTask.java (95%) rename kafkastats/src/main/java/com/pinterest/{doctorkafka => doctork}/stats/KafkaMetricValue.java (94%) rename kafkastats/src/main/java/com/pinterest/{doctorkafka => doctork}/stats/KafkaStatsMain.java (98%) rename kafkastats/src/main/java/com/pinterest/{doctorkafka => doctork}/stats/MetricsRetriever.java (94%) rename kafkastats/src/main/java/com/pinterest/{doctorkafka => doctork}/stats/ReplicaStatsTask.java (97%) rename kafkastats/src/main/java/com/pinterest/{doctorkafka => doctork}/tools/BrokerStatsReader.java (95%) rename kafkastats/src/main/java/com/pinterest/{doctorkafka => doctork}/tools/MetricsFetcher.java (98%) rename kafkastats/src/main/java/com/pinterest/{doctorkafka => doctork}/util/KafkaUtils.java (98%) rename kafkastats/src/main/java/com/pinterest/{doctorkafka => doctork}/util/MetricsPusher.java (99%) rename kafkastats/src/main/java/com/pinterest/{doctorkafka => doctork}/util/OpenTsdbClient.java (98%) rename kafkastats/src/main/java/com/pinterest/{doctorkafka => doctork}/util/OpenTsdbMetricConverter.java (99%) rename kafkastats/src/main/java/com/pinterest/{doctorkafka => doctork}/util/OperatorUtil.java (98%) rename kafkastats/src/main/java/com/pinterest/{doctorkafka => doctork}/util/OstrichAdminService.java (98%) diff --git a/README.md b/README.md index 63a413ac..43a245e6 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# DoctorKafka logo    Pinterest DoctorKafka +# DoctorK logo    Pinterest DoctorK ### Open Sourcing Orion -Based on learning from DoctorKafka we have created and open sourced [Orion](https://github.com/pinterest/orion), a more capable system for management of Kafka and other distributed systems. Orion addresses the [shortcomings](https://github.com/pinterest/orion/blob/master/docs/Motivation.md) of DoctorKafka and also adds new features like topic management, rolling restarts, rolling upgrades, stuck consumer remediation etc. Orion has been stabily managing our entire kafka fleet for >6months. +Based on learning from DoctorK we have created and open sourced [Orion](https://github.com/pinterest/orion), a more capable system for management of Kafka and other distributed systems. Orion addresses the [shortcomings](https://github.com/pinterest/orion/blob/master/docs/Motivation.md) of DoctorK and also adds new features like topic management, rolling restarts, rolling upgrades, stuck consumer remediation etc. Orion has been stabily managing our entire kafka fleet for >6months. --- -DoctorKafka is a service for [Kafka] cluster auto healing and workload balancing. DoctorKafka can automatically detect broker failure and reassign the workload on the failed nodes to other nodes. DoctorKafka can also perform load balancing based on topic partitions's network usage, and makes sure that broker network usage does not exceed the defined settings. DoctorKafka sends out alerts when it is not confident on taking actions. +DoctorK is a service for [Kafka] cluster auto healing and workload balancing. DoctorK can automatically detect broker failure and reassign the workload on the failed nodes to other nodes. DoctorK can also perform load balancing based on topic partitions's network usage, and makes sure that broker network usage does not exceed the defined settings. DoctorK sends out alerts when it is not confident on taking actions. #### Features @@ -19,10 +19,10 @@ Design details are available in [docs/DESIGN.md](docs/DESIGN.md). ## Setup Guide -##### Get DoctorKafka code +##### Get DoctorK code ```sh -git clone [git-repo-url] doctorkafka -cd doctorkafka +git clone [git-repo-url] doctork +cd doctork ``` ##### Build kafka stats collector and deployment it to kafka brokers @@ -56,8 +56,8 @@ The following is a sample command line for running kafkastats collector: ``` java -server \ -Dlog4j.configurationFile=file:./log4j2.xml \ - -cp lib/*:kafkastats-0.2.4.9.jar \ - com.pinterest.doctorkafka.stats.KafkaStatsMain \ + -cp lib/*:kafkastats-0.2.4.10.jar \ + com.pinterest.doctork.stats.KafkaStatsMain \ -broker 127.0.0.1 \ -jmxport 9999 \ -topic brokerstats \ @@ -100,7 +100,7 @@ The following is a sample upstart scripts for automatically restarting kafkastat -XX:ErrorFile=$LOG_DIR/jvm_error.log \ -cp $CLASSPATH" exec $DAEMON $DAEMON_OPTS -Dlog4j.configuration=${LOG_PROPERTIES} \ - com.pinterest.doctorkafka.stats.KafkaStatsMain \ + com.pinterest.doctork.stats.KafkaStatsMain \ -broker 127.0.0.1 \ -jmxport 9999 \ -topic brokerstats \ @@ -115,39 +115,39 @@ The following is a sample upstart scripts for automatically restarting kafkastat ``` -##### Customize doctorkafka configuration parameters +##### Customize doctork configuration parameters -Edit `drkafka/config/*.properties` files to specify parameters describing the environment. Those files contain +Edit `doctork/config/*.properties` files to specify parameters describing the environment. Those files contain comments describing the meaning of individual parameters. #### Create and install jars ``` -mvn package -pl drkafka -am +mvn package -pl doctork -am ``` ```sh mvn package -mkdir ${DOCTORKAFKA_INSTALL_DIR} # directory to place DoctorKafka binaries in. -tar -zxvf target/doctorkafka-0.2.4.9-bin.tar.gz -C ${DOCTORKAFKA_INSTALL_DIR} +mkdir ${DOCTORK_INSTALL_DIR} # directory to place DoctorK binaries in. +tar -zxvf target/doctork-0.2.4.10-bin.tar.gz -C ${DOCTORK_INSTALL_DIR} ``` -##### Run DoctorKafka +##### Run DoctorK ```sh -cd ${DOCTORKAFKA_INSTALL_DIR} +cd ${DOCTORK_INSTALL_DIR} java -server \ - -cp lib/*:doctorkafka-0.2.4.9.jar \ - com.pinterest.doctorkafka.DoctorKafkaMain \ + -cp lib/*:doctork-0.2.4.10.jar \ + com.pinterest.doctork.DoctorKMain \ server dropwizard_yaml_file ``` The above `dropwizard_yaml_file` is the path to a standard [DropWizard configuration file ](https://www.dropwizard.io/1.0.0/docs/manual/configuration.html) -that only requires the following line pointing to your `doctorkafka.properties` path. +that only requires the following line pointing to your `doctork.properties` path. ``` -config: $doctorkafka_config_properties_file_path +config: $doctork_config_properties_file_path ``` ##### Customize configuration parameters @@ -157,36 +157,36 @@ Those files contain comments describing the meaning of individual parameters. ## Tools -DoctorKafka comes with a number of tools implementing interactions with the environment. +DoctorK comes with a number of tools implementing interactions with the environment. ##### Cluster Load Balancer ```bash -cd ${DOCTORKAFKA_INSTALL_DIR} +cd ${DOCTORK_INSTALL_DIR} java -server \ - -Dlog4j.configurationFile=file:drkafka/config/log4j2.xml \ - -cp drkafka/target/lib/*:drkafka/target/doctorkafka-0.2.4.9.jar \ - com.pinterest.doctorkafka.tools.ClusterLoadBalancer \ + -Dlog4j.configurationFile=file:doctork/config/log4j2.xml \ + -cp doctork/target/lib/*:doctork/target/doctork-0.2.4.10.jar \ + com.pinterest.doctork.tools.ClusterLoadBalancer \ -brokerstatstopic brokerstats \ -brokerstatszk zookeeper001:2181/cluster1 \ -clusterzk zookeeper001:2181,zookeeper002:2181,zookeeper003:2181/cluster2 \ - -config ./drkafka/config/doctorkafka.properties \ + -config ./doctork/config/doctork.properties \ -seconds 3600 ``` Cluster load balancer balances the workload among brokers to make sure the broker network usage does not exceed the threshold. -## DoctorKafka UI +## DoctorK UI -DoctorKafka uses [dropwizard-core module](https://www.dropwizard.io/1.3.5/docs/manual/core.html) and [serving assets](https://www.dropwizard.io/1.3.5/docs/manual/core.html#serving-assets) to provide a web UI. The following is the screenshot from a demo: +DoctorK uses [dropwizard-core module](https://www.dropwizard.io/1.3.5/docs/manual/core.html) and [serving assets](https://www.dropwizard.io/1.3.5/docs/manual/core.html#serving-assets) to provide a web UI. The following is the screenshot from a demo: -![doctorkafka UI](docs/doctorkafka_ui.png) - +![doctork UI](docs/doctork_ui.png) + -## DoctorKafka APIs +## DoctorK APIs -The following APIs are available for DoctorKafka: +The following APIs are available for DoctorK: - List Cluster - Maintenance Mode @@ -210,7 +210,7 @@ Detailed description of APIs can be found [docs/APIs.md](docs/APIs.md) ## License -DoctorKafka is distributed under [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). +DoctorK is distributed under [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). [Kafka]:http://kafka.apache.org/ [Ostrich]: https://github.com/twitter/ostrich diff --git a/docs/APIs.md b/docs/APIs.md index ba92d82e..f41b78c5 100644 --- a/docs/APIs.md +++ b/docs/APIs.md @@ -9,7 +9,7 @@ List Cluster: curl -XGET http://localhost:8080/api/cluster **Maintenance Mode API** -Allows users to disable DoctorKafka for a cluster so that manual maintenance operations can be performed on it without any interference from Dr. Kafka. +Allows users to disable DoctorK for a cluster so that manual maintenance operations can be performed on it without any interference from Dr. Kafka. GET will get the current status of maintenance mode. PUT will place the cluster in maintenance mode. @@ -24,21 +24,21 @@ curl -XDELETE http://localhost:8080/api/cluster//admin/maintenance **API Security** -Dr. Kafka allows plugable API request authorization and follows the Role Based Access Control (RBAC) model. Authorization is performed by populating role-mapping in [DrKafkaSecurityContext](https://github.com/pinterest/doctorkafka/tree/master/drkafka/src/main/java/com/pinterest/doctorkafka/security/DrKafkaSecurityContext.java) by creating an implementation of AuthorizationFilter e.g. [SampleAuthorizationFilter](https://github.com/pinterest/doctorkafka/tree/master/drkafka/src/main/java/com/pinterest/doctorkafka/security/SampleAuthorizationFilter.java) +Dr. Kafka allows plugable API request authorization and follows the Role Based Access Control (RBAC) model. Authorization is performed by populating role-mapping in [DoctorKSecurityContext](https://github.com/pinterest/doctorkafka/tree/master/doctork/src/main/java/com/pinterest/doctork/security/DoctorKSecurityContext.java) by creating an implementation of AuthorizationFilter e.g. [SampleAuthorizationFilter](https://github.com/pinterest/doctorkafka/tree/master/doctork/src/main/java/com/pinterest/doctork/security/SampleAuthorizationFilter.java) Here's the flow sequence: -1. DoctorKafkaMain checks if an authorization filter has been specified via `doctorkafka.authorization.filter.class` configuration and creates an instance of `DrKafkaAuthorizationFilter` -2. This instance is then configured (invoke `configure(DoctorKafkaConfig config)`) and registered with Jersey +1. DoctorKMain checks if an authorization filter has been specified via `doctork.authorization.filter.class` configuration and creates an instance of `DoctorKAuthorizationFilter` +2. This instance is then configured (invoke `configure(DoctorKConfig config)`) and registered with Jersey -All authorization filters must implement [DrKafkaAuthorizationFilter](https://github.com/pinterest/doctorkafka/tree/master/drkafka/src/main/java/com/pinterest/doctorkafka/security/DrKafkaAuthorizationFilter.java) which has two methods that need to be implemented: +All authorization filters must implement [DoctorKAuthorizationFilter](https://github.com/pinterest/doctorkafka/tree/master/doctork/src/main/java/com/pinterest/doctork/security/DoctorKAuthorizationFilter.java) which has two methods that need to be implemented: -- `configure(DoctorKafkaConfig config)` +- `configure(DoctorKConfig config)` - `filter(ContainerRequestContext requestContext)` -`configure(DoctorKafkaConfig config)` provides DoctorKafkaConfig to allow authorizer to configure, `DoctorKafkaConfig.getDrKafkaAdminGroups()` returns the list of groups that need to be mapped to `drkafka_admin` role +`configure(DoctorKConfig config)` provides DoctorKConfig to allow authorizer to configure, `DoctorKConfig.getDoctorKAdminGroups()` returns the list of groups that need to be mapped to `doctork_admin` role -`filter(ContainerRequestContext requestContext)` should implement the logic to extract and populate PRINCIPAL & ROLE information which is needed to create a new instance of [DrKafkaSecurityContext](https://github.com/pinterest/doctorkafka/tree/master/drkafka/src/main/java/com/pinterest/doctorkafka/security/DrKafkaSecurityContext.java). Jersey then uses this information to restricted access to methods for users who are not in the `drkafka_admin` role. Here's the flow: +`filter(ContainerRequestContext requestContext)` should implement the logic to extract and populate PRINCIPAL & ROLE information which is needed to create a new instance of [DoctorKSecurityContext](https://github.com/pinterest/doctorkafka/tree/master/doctork/src/main/java/com/pinterest/doctork/security/DoctorKSecurityContext.java). Jersey then uses this information to restricted access to methods for users who are not in the `doctork_admin` role. Here's the flow: -(Authentication) -> (Populates user & group info headers) -> (YourDrKafkaAuthoriziationFilter) -> (extract User and Group info) -> (Map groups to roles) -> (Create SecurityContext) -> (Inject SecurityContext back in session) +(Authentication) -> (Populates user & group info headers) -> (YourDoctorKAuthoriziationFilter) -> (extract User and Group info) -> (Map groups to roles) -> (Create SecurityContext) -> (Inject SecurityContext back in session) Note: We currently don't ship authentication mechanisms with Dr.Kafka since authentication requirements are environment/company specific. For plugable authentication, please refer to https://www.dropwizard.io/1.3.8/docs/manual/auth.html You may also use an authentication proxy. \ No newline at end of file diff --git a/docs/DESIGN.md b/docs/DESIGN.md index a9b3ec77..4afafe78 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1,23 +1,23 @@ -## Pinterest DoctorKafka Design +## Pinterest DoctorK Design #### High Level Design -DoctorKafka is composed of two parts: +DoctorK is composed of two parts: * Metrics collector that is deployed to each kafka broker - * Central doctorkafka service that analyzes broker status and execute kafka operation commands + * Central doctork service that analyzes broker status and execute kafka operation commands -The following diagram shows the high level design. DoctorKafka is composed of two parts: i) the metrics collector that deploys on every kafka broker; 2) the central failure detection, workload balancing, and partition reassignment logic. The metric collectors send metrics to a kafka topic that the central DoctorKafka service read from. DoctorKafka takes actions and also log its action to another topic that can be viewed through web UI. DoctorKafka only takes confident actions, and send out alerts when it is not confident on taking actions. +The following diagram shows the high level design. DoctorK is composed of two parts: i) the metrics collector that deploys on every kafka broker; 2) the central failure detection, workload balancing, and partition reassignment logic. The metric collectors send metrics to a kafka topic that the central DoctorK service read from. DoctorK takes actions and also log its action to another topic that can be viewed through web UI. DoctorK only takes confident actions, and send out alerts when it is not confident on taking actions. -![doctorkafka diagram](doctorkafka_diagram.png) - +![doctork diagram](doctork_diagram.png) + #### Kafka Metrics collection -DoctorKafka needs accurate kafka metrics to make sound decisions. As Kafka workload is mostly network bounded, DoctorKafka only uses topic partition network traffic metric to decide topic partition allocation. Currently kafka only have jmx metrics at topic level. It does not provide jmx metrics at replica level. Due to partition reassignment, etc., the traffic at topic level can vary a lot. Computing the normal network traffic of replicas becomes a challenge. +DoctorK needs accurate kafka metrics to make sound decisions. As Kafka workload is mostly network bounded, DoctorK only uses topic partition network traffic metric to decide topic partition allocation. Currently kafka only have jmx metrics at topic level. It does not provide jmx metrics at replica level. Due to partition reassignment, etc., the traffic at topic level can vary a lot. Computing the normal network traffic of replicas becomes a challenge. -DoctorKafka deploys a metric collection agent on each kafka broker to collect metrics. The metric agent collect the following info for each broker: +DoctorK deploys a metric collection agent on each kafka broker to collect metrics. The metric agent collect the following info for each broker: * Inbound and outbound network traffic for each leader replica * leader replicas on the broker * follower replicas on the broker @@ -26,7 +26,7 @@ DoctorKafka deploys a metric collection agent on each kafka broker to collect me Note that as of kafka 0.10.2, kafka only expose network traffic metrics for leader replicas. As follower replicas only have in-bound traffic, we can infer the follower replica traffic from leader replica traffic. -#### DoctorKafka cluster manager +#### DoctorK cluster manager The broker workload traffic usually varies throughout the day. Because of this, we need to read broker stats from 24-48 hours time window to infer the traffic of each replica. As partition reassignment does not reflect the noraml workload traffic, we need to exclude partition reassignment traffic during the metric computation. diff --git a/docs/doctork_diagram.png b/docs/doctork_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..5955cb8e1d7e3451a7db88d060dc2fbb3e331eb4 GIT binary patch literal 27312 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 qEWBP`v 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 biwVc3_$a5&f(#J*uc+MDRH(T3B + + + + + + + + + diff --git a/docs/doctork_ui.png b/docs/doctork_ui.png new file mode 100644 index 0000000000000000000000000000000000000000..44b8c7aa46a573a003c6ea396b0bc89b2198940d GIT binary patch literal 47829 zcmeFY1yfzk5-z-PhhV`yxVyUs2^!ojxXZ@fWkYaxNpN>}mjoxc26u-`j=bU2{q8^b zsG@4mTGKP#Ju^K|%L-HYB#8u%3l9JQkUmO@DFFcBz5oCiJ1q3;6IW72AOHaW-CR^u z;iIT1v4VrGskxO203a2XkO-roqVYcX#3KX!TMisrYC7Z)8!+jS$OCN@h)Lhr~$wr_B#k@F(w1T7-1nC7zk{I1sJ-q3|8nJv~F?{hAz?dR3NbQgjFQI%a<50lZSXWq6 zr|utGUo586=P@_{3dG&a=@>37-9@N^L+@dg!G!E%8%^E6>jxvCCSsC8k$UO}Z|>e+ zA`Xn6T`i=?5UT}df%#I_sCNSaQe;x;nf=j{PJ_Texm$cfA_M4dWGjY<06XaJh`6}9 z(NmJZE5m1T%I8%!qux|H)z;+QSr$tmr*1fLSi+r;&A?0j)`zJ_&Y!51jzPM@gHoc` zr8M?YpyWH|XRwRW&C#6+7PT!*o{2+@xnj4S%4N?Tjd<+ed=qr*-P*b|tj3u^mj1o> zj~{&Ws=VoxN2{ohyDsntF7(G8-*bNO%=`f~c)m#gihWoUvq=+;ATGR-x*15g!A?;` z_p{%W-jg-~Z-ZP{tu(ErkUf7ve9vJscvqqm+D8R8>7NcI-Yp_~zy$b;3Sj$EBi>!xgzdf) zBy3arAqFa32-6IlUq4XA6zb3yU8#x_7C=IbRtj#_2{j}HJKg18iL>PgUWtAPhMA4~ z)1PdOiwjP(liLnX8=^r71qjIW_hy1z@wI1yD

P7RCsLrxvk@;V_0x5k?#Ya|rRw zA*MnL58faV_~1V)s+A31jQ!mgS>$T6Vp3xd%?aXn5TPih_}cM4X`RVViNh zpuAA{eF-IBIu)3Pu|*^4gTul)dyS@<-IA4L33d>7Xm-F%VOhia49(SbO4(--@WSLe z%X{7%-Wa4+9?k0>61#wh^iXdg+Fv&U@DY;2M|u)Aqt_WWjyHnsvd%uV;;tZ`_<_1F z&bV)t9u*&<9(5j>yu*pbCn@I9z+v%4C~`vWLMJ5}MX^a`Q47MF#XJmQnf)45)FrGw z=#mSNc@xJ<;eNoIWk|&m4crd-vqwFnSZbS=H%;C?UZw=xQ{nY#=?a|fa8i2En6;IHfoY0OcVb7 zqjN#^H?883VzqCg)#@5L8iiK=R(^-}3pNY(+&J6_O-xOQP0~#l4o_?tav}ZssAwQ&TTQw-C*c@-?uAmMh;P6 zBE2vDkWfw&MgLXDxFNKS%4cdwTt42w1Y(!U&Dgcec-z zGmtZAGYH@FEL7NG*fTZ57RBr4oVc4=`NMQ2Iz))n)KqVju)YzgoTz3^PtL&33{II% z1Is`DG&buuPc(Bei?;k`erH}bXEEuXD>rFs=2&-Q;cDSCJv9S2w@~;amo>K}d*tWX zwDqJ{q2!d~PpuNQJXbxUl8H(ApQ}ZyMgTl@TstHhq+{&vVAWv09MLySr+|8yQ~K^1NM5b0V6#$-ODemZn}YYz@ziuM&FqhM%{NB8|rSLPiO^dtvowL14U;= z^pRglCmI5^-45a_;UTonYNn;FRnq34r@vp6kgL_GW`*~Qg-8-bJ@>=w&s0(vCNqC# z-n3G?*1g7yMlT9v#kC~T!so^RLZpVziDpaG!RsA=Fi5{0eWvY(+3E_X3pWFog$99! zLivd@h?1RrGcGS~;@spm?2*<|WzM*C9}^q?9P-K1dhS?*m*En1wm^uQR#{WoBo?|z zCZBSV23N{aszml7oiNdxMwBNYYbwD!ritH?YlQ3|$$8KD(3#+1X3kbuu(Hdorz>i* zKRJpZjg52L26y;+RG>Pgk=y>_2tO`;BORpG-B7#zeamzg+2(3_F@`dha`a%yw4v;t z@>0GvtGOY~mT~o&EGy2ZrZYdgc1??<<#cDFP%!Cfm`6v97xb3&b2O_O&t?8vXW7%QMnJmB z!ege$E8WZWhWE&}RP`G}@w2)Mp?M8Ft34|_q~{9^5*T1Ypt^K&n#x^pb|Ow2R~1to0(|?9 z)teZbDx2mNG(7iPMyqLxsX0-60dujW&`u*k6cS&e&WGzV^ zW+A}#S?X?cN#U4wxMbR(Zv7s$Ce|^JFps|?se{m+49Lo!cvEzl_1X4nCG&Y?zoPss z^eo4<+x5o@f*_SQ*W=WN(SX=mZ^Y2n&^P(JEKEV7yYAcRJNCzeQ|yj1!Cu5w zKk2`E@Vx&1^O%v8_+MR|Ecr>*RA*%{avm`DZSiHV8%9E?qQl*AoIkZ{tYz??L`Mj+lv~ zk%PINlew)8@t<)G3~imA_(@6sO!Uv+zxgx)n*W{2#_>O5y$Z0~6!_7nzf} z>HmZ5&zpaf{Y$QYPsjIXFdhYSpox`+n7OryjpHk80<7%JeE*u}|9bP6(KnRpedZ@sP9xT`rczgC@LzVf}vt=+Uk3% z&r_MB8D{%By{_g_x68zhz?W;fnh}OWQoKD6r=1m#bN&uq-iy=BMjK?f$!y4YUo-`9 zX(2c&F!IwZlC|fo{ckQRe_H>2)%i{Vte6P$pSBR(;{a?i;1Tr8?x_49+H6P_!0#{r z8s1^M~J?ChA4j;^jDAP^|LL!|j^G4dbDPglub`^BvAhqq_& zx^933pF7XDM`B}Qd}%&^&vx8pFk3i>4-5*zQ7q2vd^v_u_9*{n5)6DW&a-xI`0{&J z_8QdjppT&xz;@JpT{Gb1`QhzkpN2)+o;yYGZ zp+35~H5BKF>4Qn5(kURGsxPiTz2O! z0QV#-lnWZZjeyrB`2x?j6}2ZAN%Tjd+?eyPX6N;j6Ofv&Tc7qTCwQ-IR(}QU5EXw4 z-PJb>n+^Gl<0o^2e1vhn#kGQa z!WujItf|0+BcJl^J^H#<6qY6k@ew*J&9I*FS#QT|YL6)T+0_F&#~gH8&Fy z^1a|Qx=q>u`GZWbsi>^eG5yEE@Bgmjt8QZO{J^syqzp_Uuz*bQbU1#lD{e6B>+6l1 z(Y$L12N7jj4I5<*E5ny)*C#I+zh^3J&ugG*FZVw3Iqk)`5pX;7j^dRn6~GV2QwOZA z>Bn_subRo|23WpaXzp~xPgK_~L1siGjU5c{YTxNx>^J?h|LlT=atG3sLn^Lj4Gx+rGy_8UKgbvp66BGFybAoy?LqqkG2|_z=KEuI&w>`ZmgN!_vVjx zyIH2=SvuI=MLuHU;wh!8J}>+>?RTa?cX!ip?wyCdJkaQ{fvE###O3o8o|HUkNGu+k zS&YVb-BE=7_+5wQMA>nw=LHh5j>uZju8r_eWOkb2qN4y@c{XmdgZbM(m$M7c) zOOyKzUiS9P@C7{QJsG$j<@*z8J`bm(*|{L#=KXV=w#2eeA+rfK(P{VY(zqQd?l1R^ z#4ywVM7%DDp75|Y5UsCUY0S}2C#bX0e$#G(|0Y|z$-(qCDPvuFqb~vr4@|Mm-T4B@ z?GCf*al#)@^Z9pfxR9lcrzc;3G(iL$ItBJw7l}D9UA zS$uZ-{c|W(@cDMtWyxA2@GH4o#l+8V%46iaFIOk4M6X7u@$8oQd`{1&Cl4fG*t>pv zwiy+3IF%Pd@2v>J_;jh_ATJ6(gHHJ=8L4z)23DNfdgnbSMu;jPqeND?V4>dprZ)_| z%6?N~^PA`M-R{^_A(qO+!T~?gIIQ$UAiZS`0Tb1t2(64rriT5C@7p0Kjg=J>5>yOQnJj_J^;{s%M3ShQ^&5TDQP6V+{$RwrZSi za@wEHrqW^Z0i%#liR_QUTPoFvuCLG_)nS-FZUP^<5M02fWX0Q5AFnHo%A410aws@6 zO=Q#~d|mu>>MuW7X@|HBJZ-w_ywfN>ymzYX%dPF^GjB3;$7kX!E+#jnCssTCAf~>5 zrnOpVg?f2deGxkq)coK-b1e`0uMJ`_0S}@9lmRNi!$ravp~p|8Wo6MSGC`=|7=Ehi zlbaHZ#1QxAtU@ zdrcb(>>*q!Nf&t3jirMo-n1^jOszqBP^^jSUQ?*xtP!`mhnux{BFJv1vH}D`v6w5j zj;1sE;WnqJN$4ilmqlyEulAqIB}N`RhytJk1%M*hwL44c;COs~I5E}2b{`y0q_^CGR>HT- zfeH#o!ml>g2!G!K+hc=dbb&D&+mpC(!&z(XXy5G%2GQ9RmfiyF!&K9Me!Men$Aiko z)Ldxl?e8~54pK^=8X1IQ!H_F=Tlln7mp_#^5cJyGGC0~G#Sk!@fDWf0Z{u~_$R8SU zOx%vCM7kViF`i}I#rXXxHq)&BcaC*wQkrWG{ah=Z3?tn^;V;=Qf`bv=$s7&{#PvOH zA*~bTej6Y+>ckLa0*m9T#X8Hg*BfNBYJiZHuexDZQ+Fcazo5s@5&hNto3#mG*qwB| z2(;h-rZvK8bCrEq4Gw}I)dXWUm?A}z~*l%Nx zt!}IXC}X0gWRvr)p=1v0RMdJ52@I28Ev}p%|)O$a<8)2y# zD4!~}vYaHS>4s_9wqYJEHE>#2Awrx`o}h=cH(>`8PL5~rracTl^fMXu!m^^oi+0uq zbtQgD6MVXi!$~YxuOdxk)QVQq^&(e()UYXxX{Y`NZrxE53pPU$Zow?ol86FTV0Xv~ zi9Ij8w_FUbWV zL3Na@DUL?iK@7JwMcXUX*)hX8rRw=bVKZN85S=#=o#Ifht6Qquw%8@W%6Q;HPrD)r zqnA5?PdX^OV7*jtvJ-pcfL|@LK%zT0%B7KDd447*mT5GaTty*Qzc@5%v{bBGmKqk< z$BUd&WIn91WW193u|&BjT6E`tTMRPIQcHs4$t5(~b+8h+!(YcSTv-ut6ss16%Z~p@ z7h1`A711z>f24J7iB}={57;Dj1&2BYi!Y3l43)nJuKB@;@pjU!%~u(Pa5vbxpa>z~ zP*75)Jw1zE&Koca8BgX?zN|o)dfDg@&;kNgtM8Z#X8c zLXiu>&oVWvEU)X7vuXQ?Bx>ctB{wY)_-7ZD|a|e;`*ZqazZsc_WLb~HZi5g?A`Hz09ww^L8uB@;ADd21u3nNO2XBeRvTj`v)im=%X-A=O51L})0iasnn% z{YtJtwS0QVKoG{q>gg2Hm=!vlc6LX+1*~v3_W`Qs!(7xk%oAdI6vOLe>_Q2}BjNE8pb@)po=lD^E$Q_NEmqthk*L1-98h?kvHn;-p*pfIqKPt!#r{JLXTn0D4lQIPkiWd@xD zkU@Tk>iyMw#1qOpl{6_an0*8g6FLDVrknJRgiXFZ3_=Dx7uDW`Zj4QUAOuA!Txyo4 z^M!i(35u*ARyWeMxj*Vf@qjNwq4Kutb{w2zh1o>x7V&4E&u0TfYotWs`tHisOS7g2 za!yB}Oxk}ea1;9AcW{S^8<5f=NI1j3~`(5^2=mw}6WX<#|99%g&xmEE{@m49>-fRh8RbZyK+Q6q8#&sVB;$g^O1p)@y zSb!AdALa)h!Z0@8o`jt)6)^(w`=k#Oc0j<-?^dlQW`! zWbi^YdDtpG8`s8w{Gtr&NY1wVfP~8i`*eTB>Ik;4vt?oA9`f@1V5`hIvUGnm^J?KHN# zC_e}E_=Gxi7vqV{cJs99Dd??we|==RgX}PZE_`>)lq;M$7k-jEWHccbiR5uSug43AXhb&%Wji)_foqiCz zGQ`i~G`enFPFmlcB@&a_@oN2XP$)K=BtLMDm}bcAkw3-c83x*t~$X`hmueZOKln-O5 zYQuIT<>1(gSMl$*3PO*QY$?zj>~e=VxsBy0hs)rV-fpsM*rS`DQ)hVI*e*QX;U)LeOYc-TFcn6m7;SCRuvsd3Ilt?#) z5U9CN_YCwAy7W<6Z=&$_yt>JZj-!ub7V7RJjl0Vuj2%|f+N+J#$sSt^M}}1I&dEfy z4~7r8#j(}Kl=iCV!)e?1FqbHvG-xt8$G>cXql|+c(wC%I>g{MxcD392@H3>ZJqj$2 z2Qe=*1as7LI$bk*Q*&JFP zXV34La$lT>p=~d1NJQXnBqGAHw<{-YNLyDH)V4OtYCOq~GQz9(w)D}BRy}OmspB)3 z2NCj*ehJW(W!Y5az(`#l4dHX2!|3qe_9{dPw_foJz&x!_RWgTaH=$Q_`sfsOfMTVFPlL{i8u%o0HTRKV+m8$J(x_R^~I2+64qmb#uIG z@zivGNZ4TYxKjhDCGNO5XKyZl$fxbzD)}nz*>}P4hhHu_SQ1@Ffy!#SSaKEWa;FQM z2&n^)XjTqyBAsKX`3Jr3FT2Ff@PAlJGmr8U-EkW}xjYcfxnKQ_b#TFe-TBd<66X%q zlC02$0Dtd!vM_42rMfYY_LFQdUZjHD;=Hy;jpl9#xwIsD{d<;1mPpc&dBx(nw)ROW z<}o`!JIOSloZMLuJbf<9oDOTj=S$p zbWH`$SWK&8u{anf2BPr3oG-R74$Ka1pi@}>4b7fQbw=H7Nw!Xc-$XpTs&~xT+valA zT9GR$szeB94;91DU}snZ^sdeQ^1Z_pJ3M zr9mc?082vT)oF;~YYon}Q2bfvBfODNST??<;p9v~cg}q<6CaO}%I2#;mI~;90R7q{ zB)lA;`MKD!_-jut{xlx5VPMsHVVVt#V;k4%Ncnx8;3NLxwjlsIoK%1yU$sngC)Ktz z&GE=eXM>vMVorN$bkq~|le(>LKnZ|9i+@=p=;E5nZZK4|8p8);kfU)n_9-1adMV#d zJ+bFIzG{7mgT@MbM;S=cCa>Gg38Ty^!Z4?*&D<2`4?Y^hu`0~~iW>q_VOg9xut+oyWQri7Y(Z8sUkKDF1N33BFAH6JC` z2!)3mui4A9-N9tVVXXzQET{s(*bX~SZC2h?)Rlkkp%)s}d1Uz*=II${RU^;pZ6 zw>RfeGa!hb?xfkKw(T~4Mhqc0=?YCLG7aH9${Vusz7T%=-kNAc*`>|f|9d5=f(LOx9xl>}<6*(t zK^VCMxIIU#rMQN&A`qJORtYk7GgAnK8I0GK1&>1pn!a>A-1f_nNfqL=Y1Tj^r5 zxwaB%meCag{((}jz*uM;65bj$)H*W)!?*&6*WQuG zYl}q7Cp}?R;_`6eN9ntArGju&(C#_k0*kxKn^c2Olu!<=5bXxnGB5B{zXl0$_sV`S zX{r4*Jx%OKz(5em=m4d>-Gy1ANp|w4wx9rXDF$ zV*zK~2#namdo18S5jTqK;gg($luxzqzXNzrT7U43N74EU-mXDh94?YcMrV869CHh}(3RY|7yJ`C|C7Pc1BYsd z4TRPJ8@zZ8b*~+_oa|78gM*Jgd+&&>JU%_45)px311>2~&!nWJ)$w(A|I5AIIwX7_ z#2|?_EqD_W;>18Cj?wkABYVb%?cQY0-ckdj%45^lslUcZ2u_<1JZJ-~c1II+@Zi-+ zHJr}>n4;Bcmtc3aST{zt%EzAp@*A-}&MNyGYyFq+d-rF3V|JRtJ0H_Z$p3MN|7hZw z+7Db0D*cxbFTN~EvhpCkKv64h(f^3)10Hk?Hrf%UDe`6;O0OBBQ8}Tk^|9$5DE-~~ zzG#DjV3@vlUkcw&WA_nV0Sx$YRe)Xh%@B8#+2BksqqTu=4kCm3YZ8=kf2pYdujeLB z{PLPC-N{Nx{CnHRcaqBqL9LN{J^TdzW37eYwz6KcqmQgV(BACL#(cP9Kx@8j7H-8~ zvHTuiIZF01{U4c<*j`yrx;#?e{3qV|y2?JU99>Mv%KVMaORs&Uc+IbVZTGTheZ%{? zybzTBbBa0K8-}!}Us>0kZjg`qZ?yQ2v3Y*wXjG7uKIaWXGO2&gbx6+{2uvrE=eY{? z)r}Ng)4OuJ+w~Dh8ug-mCz_z=r~kzHSu=hW3+h)r#Fu9Pd~;dutt^I9*7MQhGUQr{ zyp-ayu(k3VB}->3zyWuP>Rxk%r`tO_iMxhBlkSGHl>6d~@61?=XxNSg#nx6Y-R?NA z3aAA9hbyRx^)#*DZ-2>WT4ETjhd)49NcN(6{Sl$p4ykWpL8GLk^jwuqTRLveE4`Nc zE*r#m^0G7J5eN0zc7=X2U7DB{>`B&iJ4a1=O;1)R#P_9-Q>~@&jiBkU7`LP`>9{jo zZ}ddvm;0KsuH2iq9C@>JRA{fdkAQ69`2L3e|L^$Uo%pJrOsvK4J?p-r@GT}P`s3T( z^sGA~ma`6Tbf^Fh^|4bf5Wh|u}>v)m3AEQ=)@h(neG8sAtp8!sQJPuugWvp$oZac#hipT4orR0`x7=?_F5E)vWel1eMWaZVP2u|a8 zP8rl@msqT(II1qh{J+3lUDdpPmx4Gd-y@4?rO~+*=?BE~vOF%yF15$Ap2^t2(gP zHxkm1)G7>k(M?`3n`aGzJ>gX+Bp;}JQyH|lJdU%63|V;W4kv{_@bD?*cxn`dI_9rW z`aMZ~x{=r(-ex(dsl#SYP7H*ALFroJ#3+ynO5ywV5pKTYh1fGdCcCtknG32QJ{~#_ z-O&DW{-a`eCY4Sheruaqkn*kKBXi|(;15ME7n=9mG^&AW!NsGK-zk0>JDGC$|I}b< z$sQdoc$?>^dJlO{1;`-eA%+U09_qJaL_xU?zco5NM-eZ=JMOO_FCnI{Wb@?zWe*&& zcs#$MVY8e;lf#>1y;CxbUI*>_kPVSxbFO&Oe(jFeDX)cGa6(J?Pr}}I*E8ude zY+^05^fCBEQ3VL520rHKiAm zTA(rewc${jDF_p?IX(H^8c&5mjU!EM3S)}%jn%CX1JmN8>nq}<8#b_Q}FS$@_2 zcKT{=OJX1u0xr6-STuU>erBXJ&BfVjrr%yCrY3WUHOcsT8^lbM>M1HA>x945Fk;Vn z5IfguLYoddJ~uD83hD~R?IiH3berP9zTJal_d2pbM;XG33Yw?~RcGT_xMR%rVg zR-##j5AHcnFE19Dz+@l&Qw68|dO*%gPk3o*bDhMcRJR>A0*j%uwZl|8)8SNB8tnlh zmckJphp^=US6j8Nl)GZ45lQm_!d6Z<#KpQoSgab6gpn|)JR?)2ZMyyR0=^Rog+J(k zyg^N(LW1Z$eQB4ucWA{GoKqA_dc4@u6YuN`PLmK#7uW)qZm8=JXWfMDv|yt(l+c zm?i-3EL7&Br1W>?Qa58@Z!$MIHzpi2%rb@==~ z*$>V1tUPx$w(6i5Ep2QgoX!oi?>zRqBz@HD{e)UbJ@irP(-Ks^F7^YiXZ}PQkj~6E9yI3W1bd^ zCkg!MaN);&TZg6vrq~7e7W?U9@na{OA6D-e>!-dIgM$1Vkob43!9UExr z$agg@d27*I(&#Z+1RKX4iC9&wGrAhr-jfWsYIKs*Lopt1>X`FC0!cugb3oD=hWhu9 zaU#xLwPK+#X!(Oya4mG(2;H&6LK@bKR(FzZqS77_zb8`rDHlf(j**^TBgl_DW#C=mIR$>*}Y03F;vU90;;uY}Rhj5g-a z=OZ-wD&G`7$$lkV=q{Cd8*m7R^WzhQ3%u5pa|FIHlxozxPvf-divH!X#^|{9d!`I@ ze|3;8{N#R`Kr-iq>dMPm*3}J#P~~#*DZWe*MNMx3ALi!RuZIA3vtb0el0blso}BW&kZE0Ix9jOzHMo$M*HD2#nXknsh=Y{!@QQ5eAY!u7b@Y* z356STo$*xYaD2rtOgGdm%$<&an2*ygMm@*Md$ZdX@pPz}%zBEC;Tta69S&f)9F`H% zQODgIf-90u)djM=a8t4B95?cZ0^1ZEb&})@gNv}Hn7+ZFH}w*I5kl>d#BvP?`1qu! z8yg!YQ{uSGTd~#x?cS$X>;CZY;>ZO&l1b?3#xu4KxUF8sb)^*zqh&Iz^4q89=ka$A z`=(RDEp+{-gTO<)AapE;%2rCsd2U)K1^}vX-(3J*66qCi@GixnlveINu(3BR%4$OH zeJ0R2v`QtVB#C%MeEjYrE}@H%^l-yyKlV~c-?w9aoCWy@M~EM5khV0R>8CW`asG$3 zGJDpuJ$f}Vso5}WMLLz~UK#4XUV#ZZP_Cnk(CQmMQo$2;OA9c4DXSP?Milv_viswB z&uVh};)5}PgS&S6*|Y3Ve}^`)hJpHHx*9PvR`qR6_Q zaCeIO*($xJJ+hg#xAdDDUlWjCYw4~btlWlVz?0AaPjg0!)xRrtCOwe4H{x_2FLmU$UyNN zS#fsrm$)wG)73L;;#2h*x2j#VI{PZhush4h{lPe5c;r2E)78|rAK8Kh{Yp~PRS{6D zZ1!)d&xg~j71&f#iA>d9v~*^Spz}4ZB;c;m&iD%a&LM6!#x%lP#fXAqZGnx-3+@rn zmXnn9<|Wwl<0b1nro`iCJ-E0a6;|x-JL>WU7ZQ3Y|BbQ;fxSBgYKZ9mcD`?(W`3B^ zt8)4Urmb26&$pZbPDRdx=JU))2t&|StipqDvu`dnk^7Kd@GS?;(8!MO9K-VwVm&s~ z_pal`mwkYsSn(U;vK;qNZ={4+27jqlo1Loq`Jx*`^_cl5Q`hvf>G}_i6jc^GQ9P{> zuN5R@-f02|51_tm!vu@di}!NriQ#=d6P>RGl!4+Uv!pBQ5#KSTzkKUq+mITlpOIl& zANZKe`$ZsExlJ+0&VY6;V_bPA<#MbB+9Xbv=HCRHmPK${EhCY>mlu0b?RZi9N@zvBk)O`)sLt z_}vmH*FC}EG~QvK`{zXKo1%}6kHjECAzHHODz8nNM;0&g>FL{=6U6-?`}xy3Dk}jD z^IpI*|Jt#)#9m|5*>cKFZ(+ghdBl{I@d)9HR5(b~NhDmkIN9M-m4rdJ!|~1@hOBC7 zCB>KK8l~MPG1Jfwg%``S>!aI;5PgW&VHjte=TW<7Bi+_iS~U1%woUJ|q8?T-bS`fy z_HO4bg@mZ^i%|09SHGMsjf&&UXuj|4C;!2G*eA?|b|93^dAi~C>+GSsPAVatv>{SI zQosd}#kY(Iv12VM>mR3)d&zy?tN8f*B*iCSgZimAKc!h`HQmMZGVRL5PM^@_$JJt{ zR*q@Kf}+Qkyc_JYxJFb*pXoQFA!vEhCK-TWbu6k`+wsJ zuWoOgP&*OS?!hi72B65x;3xR#O|rdyux%c=JhV9wlfEOg{LP39H6w7kU^qu<+d=AM zXkRozT9x&Hi`B1YBIn`YT=Al`n2lo?Nkh_4nbE79NSba3qIsSK>$jJ zz6zBd@_J5J;r4AZ)nVN2;GURDsXEy9a8ghRDjS9wsnr%AH3_m}k`J%*6btt5J72s8=5Pw=qR)`-#!NF}tU(?qla0u5?uB@SsEDb+`_&JgYm9c^b|} zFtmA-#A!71qEx9?Ot4-w@j@f(%GIVEL50;7J;e`o@~g3 zT#=#!@_9~8EXA62N)j)A=#rqdQ*Tr=LR~7YLVTJ;YHE|^*i?*&%>jACiIpB$cm&a= z@Pv2xYm~aJ80n#W^iKF(Zd=5;aX%Ex(}CnN!Cgxu1aMPD_99}d?Rl}qN_Dt~o12Q= zjDex3MD#ZXtAt{GAISJ}>AtJdLD>?mBF0)s?fkZrSp^YXmSgn<=SSTcxu()VTWG*aS02X z(}y7XXvxpurmoMqVTGQsU*pzrYozh$xP=*b3;W39MWKs3k0rWF7=&TYv(xL1Nc8w2 zxa|UJ=aF4pObz9+$<8bId^!2BuSC7T8qMkW#z9>hmGD0%3p!MXxy3C zT}9UZ;7maQdcnsWx#}+e6;hy0L*aeOL8Eg)^`0RUbfZTUeawF%mQqQa-$%KY23qYt z(0hv9mf79BPEBC`{jeHKSFpT%=v!^B`S;roKSMF%RYD;n4~{2EezOoFahNc5hqYrE zqTU$ciCm{j%HK2J1MxhNd!p;~!nvC8BnO^?{2+)#k2=|8b=jfJyT;*v^_BeGj4on6 z&9toYI0FA^vq<<>DL0(@4RRa9aOoDfV@n+zyZmZ z(Ds{O{#qxkt0}9oftU!Uo9n1GOm0fH=)0vF7B}#n+vG}BW>aj39cK=KFpUQ5=Y15Q zq$JJ_&`CeI?XKbkF>|EDA!QqLphT#1J0^uGVS8cjPXO&_y>WQehGOzdtsR z=VQR0#Mh*$gmUy;f@7ot*7W<=}iG+>34q5ePJwnT?Pthan;oWNY8o z;CmQtvy1oz`Z;K6JjeqdyX3N7v9PK?dn-6qCGLg4PzRTc@S?Z|+P86@9P)kRQAeQ*mcM!IWu#%;E#eMX09g z9IJpVxANg1PK0IQPHiv?@nC$xud>O6y+9Ev&SI2Sig)`UrE`3GX_sVZ4omYHZ_fD$ zVB3TYPaSmZC#P6s`kD1mvt3r>)d7KLX0DoVbO7t-v(P^~+?VF>Y@L zUnms9vslkkrchk-=PH9c26M*Gd;#eMR6SeWp^m#sCRJfSJ`@2=A#mPzk1)c@d3b`> zGu+8Kd;Ch4*u~A7Xo@k`{8o1!p#{%s%e8o5WYB)Z;Md6nuY!yos(R*eG;ugrJ$bvI zxMfeG(vCe4e<}T{G(UfUMPTs5E4Pq{iVh8DpSmi4-n)Ee(N-(MA66*|qqW&5am48Z z$6>MkwNIFzUu1MwT^7o(giMe+ljHWaLZJBY=r*g7LV-Z2 zrP&Iy4SDP{#~2czobVfP7LMfSiO{K{@ff=&6&w=EhPPEj2zSN|%e+}aYA80*$UUwi z*ERULgWB9&inXVbv@ddJT?y_>Hs8fNRGhu-%TW90v=(`Ql5)n}w{I-GOnId(EsWrv zkEpo#k-!SEz=n2cB3(z??;&?wv~ky^G1VO5xH&Y;NwxgHV#Al3oh6`BN(K}f%w_cZ zN~8oa!{S1|o=LyJ#H!cv&)Vux5RTFm3KY2>%Z`zk{OplA@d+3#rn-d=-k7XM20E_2 ztoOECt5;f}&lU}ku$f{ZA1;MSz@Ti7D0X&w-(<_COc%;&n)lY0)EO3O0N0O$f{$#v z{RyI~TyI=P?;Kf7D|o}(pP->{6DIi>lx_p{Z+B#c797>DZX`IG7ORj()>*-~>Pb+4Nc86| zq%&T-8ZQU8x7PSRaNsN1ra$z~r}H~wyb;7#v#j_PA$IO)nf-F_A#w4}vx{ z(GEYj9H+dJh`s%(Q&p%UNpc+`-)2D4@yMZu-HxAFWTyLMG58$RF*;r@SGyc)B)J_- z8Y3R2c$vwl!g}qAx8m~#^8JtR_I#iLq&(ZN5s00v1tuyzR6pv`Z~qDcVO?Z`t3E0` ze=&}iOk!yQ=1k9OLnj}jP`6w^bYbef;M#~)Q*R{*H%Qu~BnZKn4oscx8A8V|S^3cn zve;gL<7?0&)l3ych_>L%hPB7OZ1fHTnO2j1_aHp|p5=gL@gVO(ahrybkod_uCM%rj zQ&Q3ft8s}HrSm|O?S%Q>WN4`-H&L|Jgv=@je9%tc+L-l6+|hCrU>*1>zC%2tKf~fI^y=qANVrn01vnDD`zFez@m0exNt*PcpGheyDW$f=e5&lw zv#H?lvu@)y=VexA)a$%U>70S9Gu8FSg(go{8F50`NHW9U3C=H^ed?Qdn0iX~A&MX9 zS9X8qyG}EnU8PyR{^7Q-S2%ce1skke&RD)ZfgDD=rG&Un$UeBN8;Nh8rbblU$6M^x z9&0rEzNNJSbmL_-F0?s4+r7C7OJ`n-hkn=?>X^6Kn8YU~?xcPh2So6@bFAGv@V%KW z3n5lc+XPj$1y$r2(7H|dvWXj0n$-RJM)2qqL0EHQC_i?+pK&Sm!48u)(@Um=oVt%I zW1`tGEWR+Fs;|$)YwOXkAEsOOrXh|zvdRxG+t`pcg6N8Es$KUhRj*Io|2o~ee-z*G zKb`*maH$R|6zgxV8%^~DKr~)dVzkCof?AcD)a;o83K65C%4(54tei1HpetM(y|Lf` z#khh`6{t48KC;LW-t6N1dTnm3)%WUbnS6F%XKo53uCQfQ$fBN24s><`4qvNZnDog} zrT80Yw3}(leMNhF-DmcDy#aue$)Dt7Mf+nYXjCy!Si*NEboRWBn~&CCfUtrQRA&ot$!5ZA=|o(^M!N?C)~@q|NHCmMB25fme|aKU?BE>!xv5e=4fW| zcgD39;Xi(M;(Uo%&QQ2F{}E>~o7!#z^dbCuJ0zyW@*cxpxTJ*9{NW&F;g6=69S`V_ zRp@|?dPxgVFL-azLYJf52OvKXAy~_(FG%tjOzYO)+hTpFun)H)8}uYeTq5wa*Z!_A zpE=SXWUs?E5ElU6pu~&YWzQ`9)J(ADqtX{OORionEn5@nj;2}Td4YO^BA@^1+qso! z4g=P%S(SsRX8qa@`dJ!{DS+D0xa7_EAGj84Jv$M}pT`4vz|}q%#HH%*J6P0^$-QFm9U<9YXgWIL1(L%>8P^Nr%iCx0lK4fm0SFy%n)$hxUG&fZI zvqOT>CV+*5^kR#d2dQ`&!$Wd&$$Gtww zPd{vrF!a1P_G%|}-If|>XGL0P-drPWHlrLq5p1YmrjQGbiXrHtkK4|npCLH##+=?gaPM%h@6 ze-x(fXWdNqET$giU$KhyKOAHBSl!~E(Wsw26E4JJ7LNbQll@W8`kc9^=nUzHQ83eZH{5_#9*?p( z-Wj~!SpY!siJ#Dc-`*Gdh(hKJ zEstMx$hjTz>kx1-LRhn&);ad3T5uXkzFeGkl%R5Njj!}MYPJ_|&dCy)=v|z%zOd?~ zuJl}Pi?JBYoiDzyIadN3Nv2iq7#`L`EB7lsn{%PbVE*x|mHO;MA5|Z>sFAXmSc`Q% zM$uQAY7K$QJOUu&Aw3HyuW4KV31lSXDa@w%@I}J?4)W?$2S(wG|-Ea=Q`byC&k1 zoPW%4MooKc2`t&^d*_#JZ`4q=81FOI>z0}%z9d9-@d)!tZxCJ{r*RR#9A8G$6$v2IJ%v>)A z+wBZZ7NvPAn!;AtSjPoObW-io2$)Zh4LeKR?#L%tDx2W%k4G}3oZvi-H`(?>;@9T0 zNq$%)f()(gE8yxTlqC<2^6Y(Uw;a~f{=Mbq3NtL|ge&(^6RD@+Wx$JLv|-O_wa~92 zoE8@w(PqGF5jT55KZ$nj|CXUH5@Kx-*7||xOMIi{f81c#Eylw7b_im1VhnTnTJnKh z!bDuasDpiBOqs(_>!lJYG4PWic3e(`T#J~X5bY0aL_j+>#V>ca4=O6(vWk4RAIvh%v*5=e8o7*aD)#XGavRQ zO|afC<&m4p;R)W!TCIGN{;;6iRoA3bms=|DhjvwE6<%Y;i-*AA&TYhlva&L7r!B&f z^&~j(R~=DJ8e8AmWt|74_594`@%n-A{`eFjS=O7XqMRi8vDu(`vEOglv_9NJ;pBOD zOJWsSp30qK-6l-2e=cNb8!ghs-@TLlCZxg`bDe~WQ`x2FAw-S6dizE*UDBF3Fh>MO zlTmFTTVA=^4{qF7{98qE$@S@zyofHBJ@S4Bvfs8KAn5fyga5ObddpBsI~ zHBO)o%Kn(psdJG;=PBAby!{R$IvS|9qxBJjnfu}t?Nnu^CMTWCib8-QN?Pvc0s6I9 z!`LajOq0oMi8?wV%tfn=DED^Nz5=2ab>Wo@yIeT6hMsD_!#&aujaP`3et(<8`uWFK zSH^4>&^7!v<9yt(vh!6#w(!zKa%^25f3w$|bk?@JwMZsRc=Y24TOTPDWki&q#`!_a z|ME(W*{DH`_QQ?EUjT#~v@bGKs!7seOr=M;8zm><^ZB0 zcTdbO-@>I?c^)}<4n6h(j-yHfpu+d>F>ahj1$A33BtPhs#=Lc~i0zq2K zKxVTC^}6kD4BCVCSDE}j(7fnYd#NpHdt&4pRG{s{OQ^ir?*$iC9$;I2OD(RiR0kMn zxVQ#w(#Seh29hPMdc>KLUMqjwy4ieKpkI1LC4r86srX5Tf-&xqKw0oaMlJ&r9p`f7 zlebmgPqOK7!zHgJ**I0d-vPoY-n&g*-eos=;g=qoigqjS!2pvsuv&)CZ8Fl%}<)+?XhCWscdFFW_m=RS1R{E3ux*GZ+sbPZc^jT6xBKCIu;`RG)X5*4bY; zlMULoMVlN83pK-z8J6K*M`9GVADH^(g$Ewi=>~S41D}=?dloMFl2WIag);|PL|J)Z z&AQosCT?Xah0ni#LXyGktWRDmYXu>}HAdk*8Z2S*MM0FZ<9QU^f^B(>Jr*U$>!(oh!)3+{^+|fWS9n=G3zRKQ*4*4Y zrZ5sAuHAeR2rho5QJm{>znqnOv>>$aIgM5>Y#2X(={r#4wp+pF z9I{5T=1L*9k2nN5+JL6lzLJ#}^$>x3);rydpA^Dy75yEO3za_$*%{s^Sd7K>GIE4F zS@uD_&{Y0hus8OQ>7A>GDqC9T`C?XsD3pR^Jo|t6kj_BKnr0nBp2&I7y;l}qmra4W zJXc774hzdoPXi>ACxe{mmsqHtK4p_qlZ!`6?-k5Hh_v~)^+R@@9(R(zXFIfo7_S)* zTqz5>T$X7@M8>B1OQ7T6q<}u(kT!dXoYQ-a>4weN9ph1E#k>FkTw%&%TR)>E0x(Mo zEjA$}5d@nB1FP)RaxOxGDg(X&h&Fs`^4KCRZ~9(B{x?V^JtDMr5WyRBf3&uRCy5)N zZ;xR9S6$2-KWl`tLXnT%2Tb##=i?zV@b;EQh?(>;P(q=eiHU(srzSj&>HC(2E6+KR zOivHq8-keRZ8?f08YXXV&Sd7#m}|Lzp)sJ@B4tT6MR{6A35=JYOn|x(w`n0Nf(rlJ zSxO?;b#-d9-zIz$IQ|klFn~VS|592GxyfSul4Z{aVg$Usil4ejC3n%AKCA&v7~i0T z_Xv*B^!BlwHx3u)7$f@#g7R{W5xH)-m<;`38lLadCT)0(bOeu=fnR_LN1c0|gFyDn zc=RSeAmo>R!s&|XppKK3OA<|Q#HZIgEf^soJA(2uZss9CU-8k?sr}NV2<4B`ERAeybNq9pK+KYBK{%D(PGMd-i>uc**D8 zJIqKP%s0lg*ujs1#ew+mzo#MrGKRNZ=$|8_l!}DgG(L%$cbt|NAd8cXzXO}<$SIGX45yxw%iEQ2 zOn(dU3EgGR<+kLsunCerZ5~2wTT-$~9cO=Oe z5Tnq1CMAa0zSw&hbT>C>>PnPQP|R#mlT)A*;dm__D1GDAEiJSnhW{>)1l8?J9^H<_ zP=u(=wmTjXeI6a-nFK?Ll?^4JuRoWV9D+|u5K=v9YE4x}_xW;iUvma!=nM;;PoxKI z0L&!jKpfMecVT-wY+?L1s3mIcPkr=W`G$7Vr13Qu!Tk*lQfv&T>z~Flh@*#S8Djc> zl@SwV0GT>PmcW*iIblak)3Bwd*dF!@ySEZ|^c}v3!v3v1xa%?gl%A|qevYk4VCB zicrInJCy)zZHAx7+nx#``0lX$VI%rtw>M)qSIZhoRkN=f4M7#yutBu90=Q@8A%b}U z)H$slU7tObF>Ih+@yt%V*C6wCZGT>0zUR22Z+lwKw`ceHaz7PFxi(n%s&wR!sDdSR zj^wUTRcc^q&iKcN(SLV-ZFU$7nq6)A+#F5UlrGo3T=Hn81gyCCT^;rY)Zf!g)a;yaCg9QV1Pi3lH3>>I)U zJq!!Hlmjg4+hEBi9~9IPN#j;Hb)47i2h*O`35&s_fIaOBww@g<nZ0e9= zoI}%D#Qx3J#RP1m&6D}{UYIYXvmyxpIa5nJP0BC074Q%B{N9@&A=dJ@=j_Ui~wtRBnQOh|u0=v0TTqg>uf$Qq#iZ5~vgmRXZq zJIt4Lwj!FbqOynZfl59F{dObj#sw9#3y9#_)I1VCiIbiHD99xM8h#svR7pOdD+?US z8(!MZlrG~nULC|9wt9vD2x@HRPH!$DYBg{j2aod!0V#Q?Ur|2A=}i zRxi@YLsYhodx^HYrE;MTdv*zbs3)QxNEmk-FEipazXdDF>-o%nkg+~nahSR>4Nx%O zyjw-blU{7O;9sZ(0fu#)(ynqXbycm$cjfY9v3Ye$bv3fNsBB#NKbNv-P6Xv85c?6Z z?3d|Y!TDk+ZZUA>7-&DAc)BRP_h@@$6q|!fQ>@j$o{+p?9Hphs2oRb6zn3cP$f&?=4SR$xA6=WJD^N5 z5aCZBR}@z0oQI}f?uIPzJ3ZXF^Y%?GN#(c3h}Dfhvo5FYa7eMeXgB9Ea|coE`FvEU zjk~<~$2YdnCG({BvQI+qY%C> zlVtakUWDxZcRQl$BCs4~|C}YStDF;VCvCr4j76)J`D_ezA|v!Lm7|aDm2n?SqmL)?cIYL>Wv^nHT8j(((40g}pmy zTz40oE_(|sw=SYjmNrcAxW{c-Z2wzFEgKM1E+`PMav#XhK`l;D6=M`Zmsq2u95YZv z-Pye*KRxw$Ejn3b#*RSrl8RmLB?)~e2r5Ape|5C}ZSxgX_g%`!&uI)&Iul|5fHbc4 zzB33Kj@{>2@eUGf&^jbGY$@|il2UkFg2(pm*{t(?*!&`*yvpOHei6g- zaUfrN1LHu&37~Rp^-!$;#6UC7h9AfiA`b(*bF;E+(qI36B#ejqB0;R36gow#!27+T zup7+@)45`1G4Uf1Q2DE(`Uz2zVHv z$FGo4NvAnqL(bGnsXsNg7Iv~gwxxI3lNhkfeN?GU3fp(9I{{Ks`QFeV&Q^i0-#rFK zEk&z6r?u<${}NijQ3~gK)wcE@)oA$LDiIR#-7Ec~lZaiksog(+ggo|;Mc{3(9GCe> zx*I7cRwprPy_kMX5WeCv-)0r6cc-GFz7cB}ZW|>GzusJhwroCi+~HO%8O1I;?*|)! zRJubv!=(VCrz?8Wm$G`1Vz1pX{ou#)$uX!@3$2t)s)OrUUnl=s*4WRh)*botGUlFv zR{`Qt&|E)aiT7w+dy#@QF0rHQg!s49rx$X-j!-7Fwilh4u`VO^lv53kqV#f9U#)>; zhRYY)$rw0Y9Y)J%-J;Gks%>d1OAXyAvF|Lv9kMnn#f18ePN-_lCebDBvGC$5B8{V9 z*<~Gw%Ko${H7Ne15#D1Y?i_Ynyx(*`FrHpx`b~F_V{N@X{SKo0F{fssZd$wV>{Juf z=Lad-To{^87eD004!97%NvVpoMwN_T_A`vuphMk^eh;D(G4`)IZ6iZE7iGSUTIMUe+SH}sigmKy%1!Kl8{rFrjb<;E?Df|3A#}RuwVj@CA z$2{Z7PkMqBPkV}=(Q+`83B*H57@I-}@3S&Wffj;e)PF}A80meQ9|2ttL#TEu-;rPVfcpZb5ZrfzzjOSZ0Jh`=|j6m#+ezHmxnJ0yOJ?B^}97;^%|+#Ipb zR2pJ-K5}7z98pl=PT@}T9@0lcbPKH~zHPTV%L{NVPOU0DnqCWShwTZwu>fxLh!-S^ zc})GEcU@=?>pMUG=?c=J?F#udG{gb8t)vP{<@r(T?MUe_aOZs#~>t?S2ouQwiqqr>{#}{#{hY^-NQ)z1*`$T zlHt-n|5z3!e%Nz?mjfH%o?EXXx}$bjzm(35eB?YQR+M|tx7^sLe0v*&3c&8EE}@c+ z*8f>!zH*q(=ZG5icuMJUyoA$zQWFs?7E%Uk|N1`KYpbD{s_D*+n&S6ebwn7Ajcz-3 zCBekT(5vI>K(G~sM9$kh`DN$16Gqu2%gF{;Jh8u9u6GN>h`MLz<`oU;%oEku2aq51 zC8>Ny2aL&c+E2!p?%SFl8j3WsoP14Ad#>+68C>$p0h*KR3-z%kAt&w&vF0qz&kwp> z^Lv|e*)YU-4_aHy`GWD15dF=@!`oKgi>vh`|BKQ46c}8}%>g^`&PUw8;v4#eQty53 znp}Z3-vG}3%lM^FMc3(HskN)VI*;a%idnvM6T&{o5tQhf)(H%&5&vHOcvu2E*+oL^ z|8XVtyAzT_ZZU&bjy1u9!8IG<3_=wqLdJK$NRz5M$Q##PnZApBHPxLba>x{%s$LSv zWn!u7jYO%I^<^U-y~osJ1<54*#5Do&u5V_ziul4$Ll=}l{x{>~rI)dNlY#AKWmP>r zKJ(wZuzJ^j_c5C z^8J8paa(PZ9jR7r$Ynm6trTIo_RmODoP0*=+)yPYqv~A?)^}KLd*wV<-bYOhjB5{_ zrwbBkPy>_tdtCp#0dCr1D`Ck2<dEc#k{l%f&=-Mg z`zl=_YHEK3%iPH*L>F0bFhWGLe)v6+qHM7&Bs$X)j-}oMa}&6HBSryO^+H(-+=#x3Nb8%ZqL6`N@Ho5_Ys|wodsMe?6bGRNenmke90qZ?>#zWUkxO$ zzHhfI9b9x2vwWYMRIH}R871kOd&2gTy4yk62VHG1rnWdcnmKiITDZz|sHI(jl%-ti zu#20N=Gpfi2zAYNy~E4q_7SOw@UOO5-8KoxSrC%u^@@TW_cBS~a>_|*k%ov-7Q?qs zC~+i*1gn}bP24yqjlU`@Skbb)!=H8nE}Vg$VsV(+T_QFLiS86c?w)&4FP!Ndk`8{q z9$d>C;qH8J*{N^bN&!I6-}`a2&MY_%s75J1U2^&lRr1%0s`=7pj}f(cbb#}m+X<*S zbt6ToZ#gTQ5>~x(Uo#I5(XPE^yW2Q5lzxjzW`wpSX)>fKQJvIIt`Obj1>4J=js0>7 z(M3N)*1s_{cBn6GQ5X+_d|bfM;8@P3Xk{RCSurZ`zo&_#mCpvDH4GufE36E1#>g)o z8CIgf8KD%%tt72+6WJbN$ovB&PK!Fhr%gr;AaahqtiesmcJ7@pMeL_}tKUVI&B>kY zZ*T^3f-j2I*kNgJ0bhT2C17Emuk28F{jThlY6AaYe^c>cjv)Kf%t=2SRqf z;Vqu6Dmt5eG)Mj+5)T$v6SFVswA)lz139F34o8(U0u)60^P1|`WGC$rp6Q*TbAMi? z40xIXg(MvWSjB|2hK9Le{mX$oak;``(|91a^(-#WH;;FNuZSdGZGi8c#42*HA4XiB z9McLl-QF~OYC0QN^6&XGzGs&rcOX1a83r+LWY>)d+V=35ytn*#lsxPjvkHs1exj3B zuM6Oo_lTAZsPB`3t3LP6t$u5o;ND_cesFVxq`lWaDLU`(p1 zY20=_r~2cXc;NSR2(b-BCI0~O2u$Q|ntAXHaXjPVPX~T+PG4C0N2+bAT#Y)jmY)B^ zqeSRb%KE+gwY!5US*+!ED?kzBuqIkg2?~s^l!sXThI=?2L(^qs2MRF(ATW&nesVcp zRm-oNBky?G9jm?4+wEEeTu=jZ9#*e)9+~Zu6A?zIj~d6ENwQ_#GcW>9zb&BSD(xhS z`)@Vb-`Mz%90O7v6hTq)+5l{`d>pGzKR z$;QgrR|{%?89$mGf1d|J87GWR!iATb>8~+M9W}b^Xz97Lg;a=)7sJB z+1CF|8cu>Ud0QwYyuSVHg*#V*2F^&aJWW+<%WLY6SI&x5Ryqi4%>@z|w<&&hqcGBt zz)ZPt*(cw z@f+*$>+#fS4wmoYOqy;*)9(KWG7TWR5y;(5`0@(V6M(H*MOZlP=-eP!M6-`xiejj3iN@s?b1+-3?$XF7ED z8gC2`W3D}Xf(-1rhYPwN+crLZgls3>{iX0f?`FI9Fb-X~cmlBIwUTR?1&4KT9hetQ zo&zA(Yv&td-#o4*{ZI>a)}=}3@Si1U@3VEN^+qdo;163b4y)&IQJP1E95`CcXbcjQ zc=zO;R);Y$HSiWz6XSIF=XRmNf+Q8e1;Elih9;sLzwrN_aZmR6Sf!((yC2qllqPw9 zRO_k$@O9P$ll9FKecq4BuB*tFyb$Bh`(` zY6QV`l5@gD?Yg7@H@95jKUs&fF1@d;bG?NJ;`RXL?p?pdVqa4a?q-HAzVTg?`EAmO z8=Y3Uk$=I)cpP3`>QN}Feh$01=*6wHz3#DqZxuQVnjYzqm;v*!iFU2b7XPF$*Z9&3 z=;a;?V}G5?S-2tU1Iy7^!@Yj%_70)6;SaI@E)eFDIRID5bS;kGp(?n4LJ5C#046=_ zU0bav(PtMuC_u>7{MlpH1@{)h)Z$4_#CtTl5){v z?=grAZ%!$xsTR%pQ)~ZTkH9=-)S%@z4}mkj?$)3fssL6J#JvJT0MhVT1o zlp634OC^~5<5n{=iX~WZGP|Ux^tub`C|J8|cttyMLv2l-f+SZ0g^&0u zD)Z}ampUI&r14#=DVSwHoF)Px?enl%KV{e$!kXLbe3tZae%|%^@{r5XL5;{EH{PfG z4{TJ_nV3R`8;o?r9h$We}*e?KL3DHk@u2zmTc^cqudPO8Hv)D@ACiP zb%EV;T6sfxeY74vZ-QW2^MT>$Gqh}E`CP z`Po<%Y<-+{M`CTfEa%+GQjBw2w*KaJ(Pis4?K3?xF67)#b0~7grpSKxhoD+4^sAW< zcs801|70LdqzXM<%S7?1(;XEs*7g)qPya>$PYxr0>P8Cp#?Wt!v0AUgj%4P_7X9HD z-z)CD64WUU!_%(P3y$G>zP|-Z>L zVnjj(&l+2+`qwHzJrAJvZv&>RZogR7`!|su?k;;Y0qbFsEBExjqP#+Zmkn2n`{|E=>fes-ZRCpThvMyS`sNIv zii)}Y(bjd>d1b-2KOFqZqXHe2o{!6#-`dLbiKoSUG#$gA_Ps@?w{^@Le$1rtPE0sr zoqZ4d6R&S5y8YngGAI!6QX-jKz@b{WP=l{a(Cz+ZT86!(OHg zW1+(~-l;?jvZ!a)$O95O;@pX*qmtK`AuN&oKprZFnTxpD>Hg{Smg~#uqk=^(pN3AwJRrds3{{}X5$OxJ3IIM zEP}|B4rR#M^39fbl7H<8xTj%f!LX&&wAK5#a|C%6@$od$z1S$GMwVwvnElNwU(e#Q zcH$bchbhp4Y0&^`xAte;iBi)TQVGmgcbB_TCaOi&76c14DV|5sH@drtizu4lSec@4 zBM_Hta8OxiufUwE)$!ADSh!M$Quh;xLx5t8#$Jo)}4vz?#vz*Xz z-L%U{9#!8giHd+RhRztjSbG2xx633Rz1N0F?vMs4w+Hib1Bam|d!{nzOLZbN@YGnz zB5U4z1O3vRBtz^BHB?N;7_$|G)5;1oq zzpJgj7{0zq{dxOA;F^1r0~weoPW0j<0yZ#$-^cHS2B7+io0z!QXQ??(C5l~e)SY) z)RAICx1F)3O4PX!krA_I){ zkYdr3{GaW-Jkx)t$`!qD46+4SWj9=nRQ12~{vezR9})CC{fiOU<~`j0w`n})aEx*@ zk(*TXwsFcNk;MM@3MbDJWIV;b>? zR^29B{JIQ-?<6+GI3`$X$5{N~gy1(h3uMN<PR0 zhl`j80+Z_chndw(K3SJVW#m;u;=UZ3c003lUL?30{= z@U7XLbWi5|p7i62>r5lJ8eO(B6~TN{C4=PJ`mN{W!`w{;X<%H7a?VF91!EjrVaNv` zGUIc6>+N+sg*ZMt^s?H-M*GZ_)DxdaYl+z7sG;t`^&PPfQHqDzZg)0?)D#mhSbh@g zvTby$zMIh{o2PES4gKEz11HWl-R$F2vE+-3CPY4REt&COt_NHa#bMtF`-YV`b=vJW z_S=J1`(0Y|Tq@IP0X}ywp0tcc0RH5~tG9INU?a#u!*h=TtRPj%>Z-DgE^KA&?zY;} zz^YPZhOxXy5qrH^c{t$Ga%F&*K)fziRSBKZAih}PPF_K?F?I2DXb0R;rtkB$Pc#?X zhuOX>rDz-+@GG#F7;_YSz{xaw^Vix(g$We}ALl%^OoERO^+tqR3i!lxT4~i*+&Nu5 zA52!+ERx~ra#gtX%s&mt@U!11{uNufwbkdStvp|?!>tSqla$QlJnV+n?t8K;QNk#G zEPm8EaF2j+>)@(P+f|N#Zz`ZAohORI**{M|BoMbk6jO{saA0xfg(~bpmAA^9hDMq$ z#hpGYE8ekm%f+1z)IAGb0$0p5mh(0EOF9c1yd$q5>} zt7!Z!Zr@CMQCWnGPt(BWONfH=-0W;s^W~@u8c6hnu+o+Mf&7#; z&LVETECsSH=(ILIM2sH+#bf2HrNDvHVFb9ejHQ6ZtahRLVX_a~cP^|pX6fkS6xyYIyp>AV_{K&M?Fn>UsIKUL!}Tf!{RFI;+^ zrT$7V$6yQ$*7o;2jT}AOB$KLXXJ$SB!Z0LFe90SKmG=7ueTW z(hT>0^TzQmIxe>5OPfgCDmS^OcWE1kz&^m!m>;!oA3rt6H9nQ;pEhhQQQg*qc_`!H zHK|v+kk-`ldRzYX#^&N#;^oR6;WeLnPalKtYp}}cq4BEENYxBXa=rhP4DmdSI%gzD z5v+wT!~th4B=Dh_z(aeTyq>}L0oSq#*Y|Y{^|n6gUPgUh zMnY(iYu&zU-QwN{+W7k4j*G^14Q!4X-tPW}BkpY%b{tQ0&lTXTyhU{?(wqtfLTA6< zJ$@pcKY#qB3)8_eRc5yR*GG~qhrBd{1SD{s`t{HFHi{UYGg*to>-D++nMc0=GFdMS zFH8TKF;)F%8-Uu9X8G%XBULV*Z2$rj^6~#n-}75e?jlv=Xb0E^1*q$1ue04(}nNgD!f4~wvo=a>qalUYi^yT zvxaLE&7#rLF%VPjj!db|*yV-n2=wQzYDbTWUT7Fv^3Ir=%g~$4`YXv9+-aU!yAJC# ztF#2|Y!RfTdB33cZ^ZZ6sUPSlT<%*CQ(6%Y6&1T->6st`6~FIjsz!DOHBhx)jKsQp zTSRkN&yHQt*MdZVKaL5xb+LtJr21#s7OaIeum1)Y>5jW?jzF|C@w*fYLcI+`PP2CX5OXhL%in)T;?P2#yal z1}Ki~CSWmn%&HaiDBp8jN3^tv_#=){JszRv*ny7hs88-JP9S%N>*OKvZ!hCK=!Cuj zZ9l6~B018&P%+%G+)1x@0Sw!dGvwN$*IjJ!qGUw;8o;Z&o>F5A-1?}TUH|KiPGB8Y z|7+ZC=;nUhwyCpjk-QhOhrg)gnE!U7F^7jDp0)07gOxJb3EB4`$&dTN(yJnGyC$%V zsp1YsRPyz#t)JsA^~dfGrJpNYRS3yySu?Yq>U1nKQjz*zu(I>s2U|5FXpv!Sz#arY zkOxBQb5*p!qO?iS%c*R_oavcsGmn@K&fm&6NWPcM?F7Y2|}mSsG9d6v{F11u8& z=f}|(cG-Z`<8osCOge&%haNMnY76i$@uo^mE~o5-NK$H_8$jRWB~ms#keEuCDG5%0 zj445@d?mJzPEq@UFLGvXAwkngHYJ?${)2rNnoY9X%4#k3?=D53F6ukQw}Mp^B%me? zXASk7RY8KgP3MHk8tiH+uMN!DV&1=%Gbe7>e3`pMmr?lXD$kP>2QUQ~Z6kdNvw= zU)}sBA|6NaQ$pMeo{8o_)?mWK7g_kd-{|3e4}hznv{cax+F2q}DdVUW4fUgX*hPL;|_DHUc5NgV7 z`yXaFoAmaGaF{oJd204=-BpQ7d)By(yb>ZssWi#2=R;@r&Q z*!)D#=MH69@7=>*;!*tTtZ3K6N!4_*s8?0(;*$)vymQp0vh!3dz=&TbAxt`3<0GvW zrV8|b8jiT%?1Du&tG0xsK_Gt@Pt0*LBDD|vXT?pT7rlmJ z5NI^{_;F;m;{|>?e(d3ZrRixydp?z%v@}h}t&^njSdB$L&Z{X=7E=DtmZhFmVDCmi zf9a)q)1n<-|9d9QPRSr~w0A5oUQs^GFB#W8RB32xA$8Ke^aiNu!_y1;sRKJY6i!19 zEsr<%`!T441eb}ashyLv0>GW-Wc=k843j{GLtyQ%!ZSjqEj7b03FJQ>imZngW&sQ? zAMk~&UfN8$Nm%*R80gF-N>+FrP4~xze8PrRfdRm~*{l&k1in+~L$aI)B%eFSn_QK6od04ZY8IM9xo& zz4frPu&}Ly2I)&^%irC<<>Nz^@FIUU0(JP|jc@$r2>9MR$xV-BW+tthXMjWA8m;E% zOU^j?0lwn-K#mGoS~xZriW+@44!*INz*U0kI_z_(Mg#iy#_>zvm3Lhn5#-j+M;+ev zzx^T0Jn&@(aiGJ*ygE_A)E#D5<|O`7>t2yQp3n-^PS@PnI&j$-cItxepT z;jz9Qwee4u2pc^q6#mk9#nCanj~4d7Klc_P*U3Fm@YwnjrJ^M^*;BbF4Te(IL=u>g z^yq&TcUDnxZ9$($n&9qEkO0BGahKqb;O_43?k+)sJHg#ugS%@((BSTw&aCg=o0;cX zYx?m#^xkKm+ETy$uPU3cFeJ2+B=`tNT^XzO_~GYB3j$`oTT%E-a=_{rh|!|G$*9q_ zolizNu!$nC@$KaZW+-x!^urqwYu$pV?Fd%--Bw5q5ejV(pUn{)B_H+syX<*5kp+;Kn+@9)ei)&PXiaD4^-tc;7 zaM$NpF~L)s;W@)OGiJKmfabz4gb z!{VLN;T_7Zh>32llgXwzQ|u1JlQPj_Kc(kAj2*OJJcdGvNl5O%q!*1vU>9s|`qw%& zy5m(v~+Jn#Wpy*<+lC0qm~k%{-h3p5na?d5-UY^TT|S+(~hF>*#8O zz+T`CR{o-Co{n`eNo6Qva$GYZnWti13u%~VzD;+Y*1^S`N&y+lx_h192aRMKe0v5a@)1ki z!5qjxD-RExQn5D&Eb43!USjWc9861Hf^@Z6+b|gkvaO@fIom?k3W~-~ARrjNe-ab^ zVu9VDF1_uJW?t_GnSnZ>qN4i6-aCt{T{q~ZBZgH?Rh4k~C(q68ZD=<*w#&=}=G7Qo zo`-329uD5#8Fs9zvG|?ISI&Z8OMRiOi6WCzQ&3Ukhs-Zl+$;l;@rs&z4^452k($fE z$tgnvl1=lE_eYi)$$WM4?4oWoPu@~+9y4h*_LT2_Y4 zQ>Afdn~LO+`Xq6892~WtiMb?~Iy_A2XsE+5umeL48Q`fI1df-z)~Mwez*X9kHz^6-vA%LiRrIQ zFgI_TGJd%qCkm@50}SOHKMfO*8VflBdY0y-U*&mlJP~<}HNKCJS5BtbbNV?y4|cW# zK1caC;ZYwya-Q*8pJX)M%bmkhTfR1|OXoKJS)8Qz9a+-l3b`Sc_&&TjUZxM5bW{1p zBB~0Y;>4J0Q}CO0C7RA5YQIobJm7UMtg}mx5VKWSX-dd5-ZGQpU!pR$|1A2mR$%7% z7x(~QZeL~Is09K(m%KC8V&X{g3i3b&y1<#sef19#%0Lu?DgUn`1SHjT7ktA6W&E9v zmGo&W^8DrL^@R8%2;t(jfweCGRyzJ10FEI445GIp z{>xY27Rg&A`Wbl>EOxJp*p%L)A*o2=7Ony;zn z)hWT`da4~o#h6`HYl~ce#-0oEFVFGu@*}r7XQI{&4%Yij6GMHrt^wXer-`*vG*lwz ziciLKQ+)Kd@kUj*EU&S5YqHg?9hB7di*nbTyTpuoYQOt!G92XmuGjcBZ?s+p@o(L5 z4s(rA&VvLd*fV_F-?6@G$EB}HCUp(CWEO}%ru$Cp9eGw9HECq;?_`*;ziRiBw4Fk@ z6$0h1UGq7Qe;%6^Z>+PZ39h(aFvoHFW;p0{6ZVr*bepasvXPGTuM4as&L(wX&l7IqcUKKcj37vC2qY>(bsU8GT}Z$9kP`vETG)Qw%ybBeD9H zW0k45%J~ZJSg{Qwxmydf>2j#u-jaV`Z-q;@pw7Eu$Q;|hdBHr{^hZQ}6&38C3Y$gc zVAK1zRH*d!0Oj;YNb`zZ`K0pJkG?Wx^ae3pE)A3ypuV)GoBqdN4piIHLViqD?9N(X zlchj^U|5UNV>{PltERZLmFd>2m+v;!wHY~tv#`LuK;JG+{Tn|>KvmX3Aowq;|CX1~ z*QEZ7R%9LmG#Mb`T6yoqsEhZ&bWiAides9jFJ$JFxlOGY^>-1QMGQsAWlckZzpLs{ z1Htx9{Qy6|^Iu+_KU4GzL{myy=>PSfFx&9HP>m1&wgdK$7y?wMplog4+2C)g2M+CZ z_EpL~$5UCa(ld0~DCrI?4UDuU>_6jXxF1!4U+Of_pWb>)J)WY{QNM{)5@9Ya(Y8?y zYTtu>)D7w0|4vhoAr-@%Hh@zHt_{>@85A{&rI26oZAO)$atoXf=G$OrMu*?(57FZ) zu28*!O2}lP_V@NJR(I38l5^4x8J3p9nE;}DETXF#-@`{$W9e=1S1X39sf_2Kt~s`L zDJ}aR)I5l|Nm7{_?CQloN@t}stlA^9!-k1CcXeZ~!_?wISNgp+FVw`)2`Z|Yx`S^6 zStc+egZTD#_Jd!ew}mLg$0h9%feCayZd^fGl;5#cArHdnIgbyw$QKn zDW;!#8X7S^t>UGHz(k2T`}O#t#;(mrYPI|*wij7H->7DDYAfuyRlV@KG6D+c$tvsh z*JF%|(eI7}DZoj0Q^)MzVoheJXV>;A50_%>w~gE0b~8C-raml@13 zwGkm{n;i`PD_7mA7@H7uVZl`n$kvkZwN74KKOaw$wbN>J=cT2zdgEpXftc$`z657ga*Ww`LV6t36Ea8$GnXW%$XZ(Wg6)bWZ$(uR5rH1f{64vlJz%s%l zSqs4;MRXPZd)xrW9J=62YWMLDE1#ejwgBVhfEDK4#pnf!`{NqJ%bF!S ztAo1=I{V3(;jP&_=QF?(^OUS8rvxFuC>ub&l+v!n?}#{BW8M!rjS`x(WSYlGg=E1Z zER==3=6XQUoGB7c&P+~@XE6gAz4s0b?3&KTMbTrtXDi-4Jm-2@0;HqHF6V~$f??eC z2aBx%k6!g$3&?o_vv|DKrp4_(3Icb@^F~4?C>Qsa1B<8JK%oTl9}4>ml}40M^d#z? z7J{6gB_)Er@+*dj8^LuY?o3lFuj$*{myE9WiGD6DgVWYVw2(2KjPv;Wz?N;(_ASgo zUlNO;5$;FH@j6}{&O&*6oU0>A8N4n#Q$8#jt&c+EtyJS&a$he+ipnwq|iM4VY zS<*8+3#_&uF*&H^PBI`pmZyZ;f1KRxfHn}`d{`TNWv2YJdZszQoVvTN#+eIbgX)FE z8Gap%wkKCn9xDUH2KLk<3Ka-98m*Msq`mbqMxPM-U*~AVh84T^@lJV%jnKrilSB?gy3b{N$9 zWp7qQ$f$4~0;^?*TAl3U z?5ri%43Pbx54}lW3R72CQ;gA4Yz0^P+nr(?cB7A!%iuvK!+tIuzlDhwN~`%~FzrB% zEDGVQP7$@Es|o{E?mV)+#I+c^?wvSffD{VLOe+OH8_Au*Vs2?}Fjp)}%RyxC1LirV z;A;ndtBWZtGaIWMszlWvp?y!aTyCD4f1KZeo*&cXEei8MPI5th1>BkdkAHyMgM)QV zfw8#PB4{SeZ1b?aU}O~J}*hiI*XycnfK{Y48EZ0?sa>pzP&VDUDMg%4~05r zD^EMw;|)j?Y*jusD}2+}7EIy~#fJN-ycahsSq>{{SYMo8w#Uw^docBEhu64nhw~xy z)>*9(1v;9zByOdJE;p@FdhYxuO#2n0XgLo~hNA1+L1nc^-tpx<1*A5^%Uu!f?v@`! zyie1vUR7nEy-`2hMGs*v-H1;kj-h3uMer8iih;Vc$BP@Mh6_;WRIU3j($_9-sQ4WQ z4`VON%X`?itoy|Kc|a*FpWqzoLHR{%4kL9?nMt1^?;s#?6pUCS;3wwDQ$D%6EzLb0 z9z{jR#s>ThdA=v(8e>tFvLpDw@Tv-R8YDF3`x+hi^GqX3q5BEG9LFc@iuWxlkAL<( zvXPD)%%(dC-nm_$Ri1Di_q_7koVfryWsvT|>8>ovcdSV#v0gPYaK4NcBdXpk)vllV zZa47ycF8Nm&V_~Z3hnq4863yt;Oxtew}3xqZIi=QabeDd4(22twPkk%FcN)q<$nEDyWV$Pgg1 z45K}w(S)ti=zbc-;coB>$;Txy_x>%-FDrwKq}zeH!-qZ`;YMgNo5*@&7@lPP?&?w^ zEM9-$(UXjsHGX3hhl0Tml~?!4nE$Q$JD@p6YET8sZ&pjo>VD|<;5IBowM$Wu>tsVE z{FYZnQG2RtS5x4%US@d)d1LuuEk-!VaNCKfe76hM3|h--@abU2oF!hOn1#S?9!!ib z*3is5&=^vL#DZZcD3=2>CuC~PU-8q}nTZhvd{D;_dA4i4nEVGzus9m_y}UfHB5{Tv zJo$ffmQ<;O0)*f==4u?>ea z$20b{JS2oIaO>Hj+Cg?83ANJ9hr%131kZm<;XLkOODke+|EzDm@fl=Wt8cyp)W*XO zw;W1p_{eRtW$$fRZrmn_rOCoLKzQwnV?a+G?i_tnL~}iGmbLQW>>|)^%crQR6w{a) zk$8L>#8ZHnBGaUpys#)8iPC#K0{^l8?#ug?DdM7{JsOjx*ti_}gPnj*FT~5$J`Sv? zY?|tAS>Z`Oc_ai!M%*5Nddd)baOmZf0~@pQ0cLTym?ZN7`k$3Hmmj^xDKPBoX;OO_ zbzngv%ig-NERi3<&f7E1ncoxk&5a;3lfX3*>+nYCaiF#}?Q-BrUuTWoRxI&%7O7Zx z6Kyj=2d}cUl)W0jx;tt-%f^mz2&_ek4BaWsKEsg5Lyf}%m`g?f+1|hD8z+Xr4kn;3 zbzT?7BNQw!1|aWc&3Tf=E&^G?Rg%^0Z8|&wfG0By4tvL9uU*Eid15F2_pi~2vC!`$?=ebr*VisEajrSICbzLEK;v`ZvNV^aQcw?_nD=wy$+TuJ{GeEpo; ziOxJ6yh3-sn+HoGI_#Tdzi{Iq!NUJakucNBgs|xg>!KpcV%%qZGO81mUTtlvr4kym zR<4wFPuo#8W`Dgmw>|7SBwO9N@Of;POrL=IXI|&$L~72Ti+O3Uo$rU#Nyu_UF6xzA zMZ=2}voL^-a1GxfDvBB>MiDKayF|o3gpl!bAh30BggZ6;B+r+G-HmuI@UW`Q{{gKQ zSFHX#g`w@`swPW}HdUo5h-8!&&F`fsAAmRHJdoeQ)r^&1D=fj~gX!6znT%Vst`M3MwS%+}Dzcr#V+kR01 zJUtdT|CShkeb9(bXGgBx7~V;NH0!h6`=&~=X)v9$qBW zOUVXF{-JetRKNBj>Nj@@1OZ~b@OoX1&}e2-cP*>h$^O7<#^YGVOM{2&0r$+uMa%6Z zxil&0RkO#&OsvF+&062mpV1)n+B4uUCP9#0^KJi1Ey)k2#yUdv6pzc^+&5ZxdmYYq z{OuTH)#uR;@Hm;b%lCMs=Rw+|J+=v7?7o_`ge6Iv%8m(fv^&y`5^s#KCKwyIQsLSa zmlY6lRH;6MuB_w?jf?nBmh;ow-VVIX{# z5u1nf^=44WnXJ7?<kSZXNfr0!0(Bhs0W^*LIaOAl!rX}HOzwenvc-kv!FLNPyB}O;nka9xOv>OJX_WHs_sj z)=ZON3e~8>GeKDy_D5uj0zh$_An|;OPl+i~s zXT?c=`Y~u6$;MJb_$>GF_>kO%$oiPcVZ3-jRtDt|1UfDZe7v>72R^v$Lo%m>EO7q2}GGkF&eRsrSufODxXcTL5m{0*6Gz*#HxGbk1Sf4wP~W0pc#ZyiX={GFhZ z6I>^`W+K5d@bA8F{dvpt>w==cH7pr_8rqlQ_@fW_XGp-R9q?P;KhDg8&rtt7&Yk$v zkW=Be+`quo|L@`4URm8)!SA&_D0%tL8vxhRJAwSN8}^?nYdiVFwgk(PAr-Z-icscq zH!gn|_1)v#>_42#91)742_jM!>r&0Ae7M-gOHo-F0o9GGL7l%z+=3%U6%)-XCzBVI ziK&osFke(%v3zbC?%y%(84=GFhf);Fz9EWyc(&}7mi4_d(pO^C2letil_6MncOy6@ zn-wyhb=m@L`F^txhye&oWO;+u&MoE={~20fHb9dS0vQtnd;8l&JIiA$)3)B{h$^E3 z{mKI79&u;Rn&-bbloWb&o<<3{^-z5{qowazbxjVkbrRz*ASo#{d?ALfA>)uNF^rf(P7N& zY?9NC9!(>xu*nIQx=gmjQ}-9&=9TLrRh)*@^zAHt(5+E-uNJsmy&KeGlF z!8gc4NLS|{(NKy#w6-cq;ce}J2zro8w@w56)qfAXGpFC8AO|0I{#Eyl{_hqBjnxxr zD1UaV|3;EC@NNmUT(&))%RW_5Uex=jC)3CNr7JzgC}vAGQIc=IM{n{gLJviqvcf<8 z1k;p7M4=$rpo;D6Uw6IB1H=6%g?=DRkOv5hi+D->$M{bGK3dY#a>gc9se_#z_%b- zLSukjxT50Kydz)CL~`ekRyNC@4T=o&OWs$wPw&J4^b`OdZ3RscXqJqGY%9gS*iAe~ z%fZ#dy)YI8lhI6DI|q|{igzlSO6Q;uAn9bZNo$At^6OU)nzAb4hHlir4wP(~_m>^O zb(ZecNbku)Iryuf5Tp0v%dPuo-S(hsFvETo^<2l7qB& z9q!58fZhGXf;5i(u^Q45e{NL%XVwE3{(Krv$s;%fG4>VTa^n7^0tcT|DbW-67N>n? zn5|;DwTr%0z8)?J9U6c*Akt(!BSEY<++`dqG*R5icRsdeUfsa#+a`%XX=ORrKr$C~ z30?}H^ha(}BPhd0%vvfOAX8}B4g3H$G!Jyh<3rjW972=6oG?izgbk1!BN0RO5QQ1y}Nv}8)IG8k^JJ#_op4)daEVH2m)Pi zG~Xd@iRQ4CbX~pc+;Qt#qUFPz!_XxubLUqYkfb#eV+&4fDtdz* zPE4yi-B_qD2-Gw1;f7XgMGxfE_qx)W+If<<6N;rlS88t~1li-7iZj%#YLTWA4 zsfQMl7XF2-x&z^H4@pvKiESLUyq?Z}YK2MDe2^wHZ3Cr>xE)0!%At=FmBF(w*V++BwYm57Wy zm0V}n_XGESk9g$>bQS{T{V-XgXg_8v#ycEH5Nr1jt2_lZHC|1gTnYUA9s8b zF!vDkNbZf)&O;ZtTOJ}o?k{&o7m^&hIj=CLj8o!dA?h)3s{3dCU0VyhG7k3(K{Ksd z`2qIp7SQ%Xv)1DyP5I4&*No-Ghu6KydyJLB($;9RNtm_#w$RhVm^tG* zp+y0@KTO@&EGvIpUk(-V_ibzFg%x>?)i%u{W|d}3ih0&%w2>Tv1P(Dwd+$0B#u=)D z1Q{~H2qi6(z#s2wO;RAobg9SiQtVqZudfE6=}L_AI%r1p-$Ct(P{=ibnm+WhHZl3m zQJSY{S5I#CTcG2)dqN1KQf13JM8O{JgWGwy)sr|VivMfNbK=|#FGcU61jBMO?D<4` z9PGh?q2ZL&y8R~2&8So`?1XhIZ?9OrISGvoKF+?TO?1(VN!$6;gw-};n_>B9Myg;K@Mb*%#$+w>*GDzFgi`JHh$G8;P8G0H4Pm z-XcY$&MxQ4W+UQM#|?*T%QGmJ&|-s9(2kUJKr)0(up7dz^A=xMCnIjSnmBIm*^DfB zvDCKnJZ=FeIJb4G(Ki6PhK!;tw9piZjUL=&&y zKQy8Bew6J4`8&ThYm5&l`9f>0go0I@AwLS8*PSPfqh(S08|SDE`?e>4tkyy!F6$7k zHR7Ii*kW{4eyypqS~9H^9tmI#rYZ)-smTg!1~d8Uc0p<_zw%t&usn<{P%Kjr`U~jr zf?gtmlr*%XPc=fuS7J+1YTb9v$`go4{8!in27I>m^7OEtQ>{d5N_s41883^`wAIuD z9CpDWXl&Soi`ISVYA>iN4cl4#O|aO8#KpH#gL+kbh_vs!9(-qbPJUDl%-0X?w6S^R zRwQHUAAY1OvnT7ufPOB_^=oD?9FhRFW|SDZFNsxXbm9wEZ^d=_c!2^kro^Dy$uyw1h%VTxgMzX$EhmM z9RIf`g2h~vPz;#me*T;`YZKfY4yASyJc+Aj!b=#4b_^)6V&lyV=tYL@b-SKI4 z+8jk-n0IgESRS7=@O*~b)!R5oeY{-yatDM&>1{RJ-{H^Y3^kJ47HZUyTpp#D?uvc2 zL>QT)MiptcC)~2r218162l<_gJ@uhcpB{~@`~-si2$63~ZOCS1h#2kfuU8`d_h5q(peGJN0JK19kjY^dPiY>^ zsn_L}Nv$`Bep#=fqsz(03BE%b>f&F?3|5MZ{RDyoJ+yVMG%UA18p+-(zWn;Wf+zUo zLBlyU~z`P@4Habg9tqW6^ z__;zWl->*{V;jXWKeW>63;7oQDf*xy)!SM}C)(nZFWjE&79U}1g8?3TKxqE1CTFbA zWL501;CyJRmg@vHhbheZT1P$X&^Wxn_pIC|b}-)DR+?#lc@D`v32+V87(Wt}fq|sR z78f;u+}}OE?jR6lFbu4r{uI){G{XInnbqYnHKF}c4GA8Jc8JkKqp8`D`2WSEspn0|K34ObM3eTeY2kH1Eq_Q;{^$p*E_?$ zhq;Sp0>46}lbACd){Dn77wyf^*IA;|A_tvk#Yp(KKEJEpxX|rvi4i`P{ zI`okGHmZU)eJ0mrRqi3#y7uL?5D}JRxu_!QO7AygkFm#o2XMW@hheWVjUfWj3zg68 zqq)-LN`E``dX)CsfZXLypm-{#&6tv^iMXL9!cJlmNPbMq!-D&b*GM4y%@>pBVSdLz zbMB>@C~BtQoe_Y+v@tV*cem2+V&-Y1^i`jPoJo*)SDrLrreRYB$7J`s+GkKaQ>Mn~ zUjAm{Fv5Z2+2ANj4|IO+35s=s1sz|v4w1I2FI8*Db$4Ascg%Sfw6sL;#dAEV>r6^B zf{f!W5h@Yfb7g991v0xypfL^Ye>H3#VkO5R8v3w!rDBWD7R;2$urJzdMs-h4h;)zb z8YeQVIyer)lD>w|+2I8U$=iDzpOQM97H+n@T}2EL1Pi{@;Q4tvQN4JX=%2(wAr&ij zI=d4fTp=i7Y=`o<*Io7?lI~Wg#tjZc7;zU*Ll?n1UYtZv)lzQB)9K+?P!TN+pXpS& z#_Y@@U_CDy&ZW3K=$il-iKelCx6wi4AoPbIs_IY1t#NyNK>LanD9JfqCh{^!Cj$Mi z=K}-RQY&+Xs}kTWJgP-2@OCUI+j>WQLx>+{=qlG1wT=dkw3Em9%4Pm=-Do+>aWrwt z;|gn*x}=XesLr=kg}7g_pu^!^M9AQ{%e~Juy%QkEdtDO@RgQ=oKADF-xpNwl2Y<8c z-n+ni)dJUmjh~tIzCKFSV(_|1;^X$JyA|E>G&MQI#%{mjh*}lHW=fdlKJlj+>guR^ z$Du0OL^Ns>$6a8*BO@n+(W|*}zkn#{MZJX-e*g!*iZpEN4i)_I}#vR73>UU{s5KH7(4C+E8YgX$lZK5>F zMlXu$Uqa8_a2@Zed7&Pt`PV17xd(f7ZPlCDVVO3ac&b|6^JlPa=*Ks-0sP>kvHzo- z%bW+qZbH!*7I>VRK_vBteD&5l4@Kxo7iBMI^JWf_Hj8yhDy_43V5Q~<-+CENO2{|w zb#dMEUQXa%?Z=zjrrJXdKNB8G9!$M5Vj1V}meB~|j*-vG(k_H;RA>u%P%xO$wU9N0 z&jJy?OYN(CHtVnY$V*O}?@e<@ne^%{=)fPMk@lHALQ&?*VK`3Pm?s(}vi$lnP)4`D ziL4p}gcYsCM^>GZge7*pif@X(we#M>d4COvbHX6DF$q0nCA_1Nt)HIS>#0`va@_)S z28d^I-Cxr}B^~A99R_8RGrnc2S>hI{JNBWiY!}CcgEi(LT9q|fF{9k)hGW9eoBuyJAbzxALS-kMEW~@0wu+oN6 zz)^eC!|KCGbHjUo)i&E%{$I{ly4shxz1Xrc1~k71@P`knEy7i5+nby77^phJM`FE0 zGLPJU{20zk>A=z(_Hx=w`$8F*A{t?iz{AbBSaXKu$Z99LSxYxOa7>#b<*@%8VWO3o zz~fR7|JA<1KzW|0FQR(@SNKETi^knqCg8+HZ6mg_A1-^=KOpy3_=>+U1A0i6Yv;2A(f#!k^e5FNR zh`+O|p-_D`12v&$KR=Jje+mC@O#e``q(CsA+5V)YQW^YjhyGAc zG@vJBscJ9gF|P3qfzxdGRh+fiSw4Qu-@w&C{4eyBNDqoZ6K<70P`2*X_L4w*n9J@&{Ttn!3b8H^ z-GbPBSC`J9N1))F(;%}ps|i2^I@4;Zp?cQR;-8sD6QCC%R1U~Z{v@_tOsw%^jJF3e zIQI6*x2VpNQ}UBgoBt~mR6MwHay$g1bm?FH22(m|h{(LDg;HnfBwk@UCPPK)z|34~ zg{aE^nZ~&>a1G{2x#_92Z^kT!yDHd2VMP2X0^5s2T>Tkarbj6EV8N-&8b^nRc2@?^ z=exGW8Kf=t3j8gOieL*yZ<)TfA*T&naPa%+iZ_tkC+6T}+F6{eOv-vx{{A0R$>alQ pnn3b-lvQ&4Re7xEyUg})$avYPlzJcbIeYK( zeed6w>-n*GV&<4*j&a}j3|5f)@B)<(6%G#W#Yai8&v0-EFgQ4P91s$4hfEAf2o4Ul zz(iD3;iIT1Si#oH$i&|kzAb=HKh4c?X_ zbDZqI{DR&iP=*9iiMI)3KktF-#m9PvB*v^u8YL`5fcy+!VFsCg@NiPj;=tKq@qTxr z{*m{p*|iV@Zk;hsC<;*;K59Vz6_Rtq2qi_}-o{Nuz&mVUMO`Dbp`G;S|8_EGaJ?S=;ll1%JH;DZ&fce5*5ZDbNW(!;X7J@tnd< zCtz*s`W(Gy;N)T^B?_$Smk#emQ>oStg_ER|OiAmG_+Z})^^?6OCMDBFYR52V4E41} zY7hPXeQMyC-0wo~L7e7ciCw=liCzVkurp?7uXu*{UrzmW-YlDU+f_aHEGR{ zbd}>x9{ohO5rZ3DEYT`8M71cnv5wT^=lc&)yA+eyRL>bFQr6!^kk97dP=alM>U=V<)WBw|sJ9~{Fb8E> z80efp@Je*$y3!QnOKOtGdeMS~3;x4t*?Uu{9n|#}e#9r`xp=#X%pNmbG&UT&aiw^B zRXx7TroXUd6;;&p5TRFJ5%_idl#HJ5d*OaA=K{fzgK>%w%v+!T5(162xs(yCdn1(L z?!&*zAe``_Tz>x^MWdD58b#|_tq>*@F3rb-8FA6eh8ZOn3~CpC8HD;y#59W20F)?< z-V1LV==KBr7AGWNm0W3^yjuBBaB;OKBkr?{v zWoUhZ|M^ylSSn~XsP98;rl~^sPfjJ8UD5(Fj(|Tt-I@7cd1gTtftMnInKRqf+u_?H z_Rs%-8VKB3FSorJG!KFb&xUJ0b*jyjw-LxvYaDMvD*W?}%NCSYcc}Tn(^JKcRvV zE}|cKN4B4xa9@AE`*ipGPWz78BLply@@5(b0YohF=0}iq(1=8xC_aS@c201En5!O$ z#k($1O~RZCLM=e)0sbaQNJTWpiW3xa!#6U;;GN6x(}@`eOt`3qJZ?u9eQzZ^$jx>$nS@G&Ad+r7{{$PkiiwK#js>;=8ynHgHBbD^A zk>8--y`x{ppd}wC48C@o#C>)A8ex`ia${0FWjf-LDLZ2H)vo%=)XCIyZ1gwE)J*Pp zCR=7D?uSvk3C%*)ET=DIg~KEA6H9qZ`fx;Qgw`);UmW7M2dD(_b+`vK zg|dWNygtL5qCaG#AmS#hD&fj=JR9&Q(=8MeeYs)18&Fcm#SEhs}T8!dy8$j1@O za=|&sV!`HaWu(*1S;*dKCC75bykxLw`Y_Z!LNTe=(nTpmKc%Kma{u)TiD$OW$&v@)-lWq0P|ZSc`&#k8>^iVf#1-5wqd+zqJV)&qv0D zU0=Ay-3#A(AUB9sb-)LDd~o;>ryss1O(RPap_n>eR@rRlbH=h&%lL*-B%X~iUCTn7 ztFET;$2{M{=|aPT;k@Vq-TW8mS7@gzJ2cEy!=>cR{Hh ft1)h}S48F1OGtF695 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

r*?uhB+V`zy z{kWL;$PVZL-hA`NrQ*E5 zsXV{7Cuj(;_dhTyr+@nW&!z{5ZPpl5ttGv_=j^v!WdTlsgO0xHnt!hU6oXow1Ueq8 z=els()IdE4J|Q>T-ytEM4)g1J8pS$=_sod$l+Tr30T=5Jo@jNpKhFUJCAhR+v0gvG z-HNyIdmBIq-YFXAs>TH0(MkFX$lAjJZl0qZs3PdS;VHf1@60PG&ElA3aEuW&CVy@T zg)2gf!Xyt+@+;RE_oW&=wV;w;A1=ke>?Z$?FK77oL_{gTcd@A{N zqUZV#hGthqUKn-4R_q171OoK36=-v1J$%pZluNJ-KYHfRh2#SXyV_0z7NoWWg6XWa zjNtI5@LW2~3hWZahQszq47#HL$PxU^IpHj-e$!w6xTkl`;p<;(H&_GcGU*P;LMpMURHh#|r_E{1xFa{v4hK8KteLwErHux4LUxgX=`g7f1fpGgGwiKiP2NK>;g3x4O z@==_uDoXt`;}KY36?ilKSw`TgOb3ggAVWXtPe#B72A+>n|0Qh9tw0`tC6Mi6rmOWQ z;wZ}p>#zJ?kC<~_0l1T}I=HUXp9KH`xQiDT{{C;WG1I@gthQ bsULENMRgPYt1h8f;6qJCOSx3hBKUs*jr!Sb diff --git a/docs/doctorkafka_logo.svg b/docs/doctorkafka_logo.svg deleted file mode 100644 index 79b3a830..00000000 --- a/docs/doctorkafka_logo.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/docs/doctorkafka_ui.png b/docs/doctorkafka_ui.png deleted file mode 100644 index 96d4a689770930462d74699b5c8112e7a4427bf7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50491 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 diff --git a/drkafka/Dockerfile b/doctork/Dockerfile similarity index 61% rename from drkafka/Dockerfile rename to doctork/Dockerfile index 17e4d7d4..e9d2fd49 100644 --- a/drkafka/Dockerfile +++ b/doctork/Dockerfile @@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y mailutils RUN apt-get update && apt-get install -y sendmail # Add the build artifact under /opt, can be overridden by docker build -ARG ARTIFACT_PATH=target/doctorkafka-0.2.4.9-bin.tar.gz -ADD $ARTIFACT_PATH /opt/doctorkafka/ +ARG ARTIFACT_PATH=target/doctork-0.2.4.10-bin.tar.gz +ADD $ARTIFACT_PATH /opt/doctork/ # default cmd -CMD /opt/doctorkafka/scripts/run_in_container.sh +CMD /opt/doctork/scripts/run_in_container.sh diff --git a/drkafka/config/doctorkafka.properties b/doctork/config/doctork.properties similarity index 60% rename from drkafka/config/doctorkafka.properties rename to doctork/config/doctork.properties index caa4eea8..8eb07059 100644 --- a/drkafka/config/doctorkafka.properties +++ b/doctork/config/doctork.properties @@ -15,80 +15,80 @@ ################################################################################### # # -# DoctorKafka global settings, including the zookeeper url for kafkastats topic # +# DoctorK global settings, including the zookeeper url for kafkastats topic # # # ################################################################################### -# [required] zookeeper quorum for storing doctorkafka metadata -doctorkafka.zkurl=zookeeper001:2181,zookeeper002:2181,zookeeper003:2181 +# [required] zookeeper quorum for storing doctork metadata +doctork.zkurl=zookeeper001:2181,zookeeper002:2181,zookeeper003:2181 # [required] zookeeper connection string for `kafkastats` topic -doctorkafka.brokerstats.zkurl=zookeeper001:2181,zookeeper002:2181,zookeeper003:2181/cluster1 +doctork.brokerstats.zkurl=zookeeper001:2181,zookeeper002:2181,zookeeper003:2181/cluster1 # [required] kafka topic name for kafkastats -doctorkafka.brokerstats.topic=brokerstats -# [required] the time window that doctorkafka uses to compute -doctorkafka.brokerstats.backtrack.seconds=86400 +doctork.brokerstats.topic=brokerstats +# [required] the time window that doctork uses to compute +doctork.brokerstats.backtrack.seconds=86400 # [optional] ssl related setting for the kafka cluster that hosts brokerstats # can PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL -doctorkafka.brokerstats.consumer.security.protocol=SSL -doctorkafka.brokerstats.consumer.ssl.client.auth=required -doctorkafka.brokerstats.consumer.ssl.enabled.protocols=TLSv1.2,TLSv1.1,TLSv1 -doctorkafka.brokerstats.consumer.ssl.endpoint.identification.algorithm=HTTPS -doctorkafka.brokerstats.consumer.ssl.key.password=key_password -doctorkafka.brokerstats.consumer.ssl.keystore.location=keystore_path -doctorkafka.brokerstats.consumer.ssl.keystore.password=keystore_password -doctorkafka.brokerstats.consumer.ssl.keystore.type=JKS -doctorkafka.brokerstats.consumer.ssl.secure.random.implementation=SHA1PRNG -doctorkafka.brokerstats.consumer.ssl.truststore.location=truststore_path -doctorkafka.brokerstats.consumer.ssl.truststore.password=truststore_password -doctorkafka.brokerstats.consumer.ssl.truststore.type=JKS +doctork.brokerstats.consumer.security.protocol=SSL +doctork.brokerstats.consumer.ssl.client.auth=required +doctork.brokerstats.consumer.ssl.enabled.protocols=TLSv1.2,TLSv1.1,TLSv1 +doctork.brokerstats.consumer.ssl.endpoint.identification.algorithm=HTTPS +doctork.brokerstats.consumer.ssl.key.password=key_password +doctork.brokerstats.consumer.ssl.keystore.location=keystore_path +doctork.brokerstats.consumer.ssl.keystore.password=keystore_password +doctork.brokerstats.consumer.ssl.keystore.type=JKS +doctork.brokerstats.consumer.ssl.secure.random.implementation=SHA1PRNG +doctork.brokerstats.consumer.ssl.truststore.location=truststore_path +doctork.brokerstats.consumer.ssl.truststore.password=truststore_password +doctork.brokerstats.consumer.ssl.truststore.type=JKS # [required] zookeeper connection string for `action_report` topic -doctorkafka.action.report.zkurl=zookeeper001:2181,zookeeper002:2181,zookeeper003:2181/cluster1 -# [required] kafka topics for storing the actions that doctorkafka takes. -doctorkafka.action.report.topic=operator_report +doctork.action.report.zkurl=zookeeper001:2181,zookeeper002:2181,zookeeper003:2181/cluster1 +# [required] kafka topics for storing the actions that doctork takes. +doctork.action.report.topic=operator_report # [optional] broker replacement interval in seconds -doctorkafka.action.broker_replacement.interval.seconds=43200 +doctork.action.broker_replacement.interval.seconds=43200 # [optional] broker replacement script -doctorkafka.action.broker_replacement.command="/usr/local/bin/ec2-replace-node.py -r " +doctork.action.broker_replacement.command="/usr/local/bin/ec2-replace-node.py -r " # [optional] ssl related settings for action report producer -doctorkafka.action.producer.security.protocol=SSL -doctorkafka.action.producer.ssl.client.auth=required -doctorkafka.action.producer.ssl.enabled.protocols=TLSv1.2,TLSv1.1,TLSv1 -doctorkafka.action.producer.ssl.endpoint.identification.algorithm=HTTPS -doctorkafka.action.producer.ssl.key.password=key_password -doctorkafka.action.producer.ssl.keystore.location=keystore_path -doctorkafka.action.producer.ssl.keystore.password=keystore_password -doctorkafka.action.producer.ssl.keystore.type=JKS -doctorkafka.action.producer.ssl.secure.random.implementation=SHA1PRNG -doctorkafka.action.producer.ssl.truststore.location=truststore_path -doctorkafka.action.producer.ssl.truststore.password=truststore_password -doctorkafka.action.producer.ssl.truststore.type=JKS +doctork.action.producer.security.protocol=SSL +doctork.action.producer.ssl.client.auth=required +doctork.action.producer.ssl.enabled.protocols=TLSv1.2,TLSv1.1,TLSv1 +doctork.action.producer.ssl.endpoint.identification.algorithm=HTTPS +doctork.action.producer.ssl.key.password=key_password +doctork.action.producer.ssl.keystore.location=keystore_path +doctork.action.producer.ssl.keystore.password=keystore_password +doctork.action.producer.ssl.keystore.type=JKS +doctork.action.producer.ssl.secure.random.implementation=SHA1PRNG +doctork.action.producer.ssl.truststore.location=truststore_path +doctork.action.producer.ssl.truststore.password=truststore_password +doctork.action.producer.ssl.truststore.type=JKS -# [required] doctorkafka web port -doctorkafka.web.port=8080 +# [required] doctork web port +doctork.web.port=8080 -# [optional] disable doctorkafka service restart -doctorkafka.restart.disabled=false +# [optional] disable doctork service restart +doctork.restart.disabled=false -# [required] doctorkafka service restart interval -doctorkafka.restart.interval.seconds=86400 +# [required] doctork service restart interval +doctork.restart.interval.seconds=86400 # [optional] ostrich port # -doctorkafka.ostrich.port=2052 +doctork.ostrich.port=2052 # [optional] tsd host and port. -doctorkafka.tsd.hostport=localhost:18621 +doctork.tsd.hostport=localhost:18621 # [required] email addresses for sending general notification on cluster under-replication etc. -doctorkafka.emails.notification=email_address_1,email_address_2 +doctork.emails.notification=email_address_1,email_address_2 # [required] email addresses for sending alerts to -doctorkafka.emails.alert=email_address_3,email_address_4 +doctork.emails.alert=email_address_3,email_address_4 # [optional] brokerstats.version -doctorkafka.brokerstats.version=0.2.4.9 +doctork.brokerstats.version=0.2.4.10 ################################################################################ diff --git a/drkafka/config/log4j2.dev.xml b/doctork/config/log4j2.dev.xml similarity index 77% rename from drkafka/config/log4j2.dev.xml rename to doctork/config/log4j2.dev.xml index 77485bf6..547fa00f 100644 --- a/drkafka/config/log4j2.dev.xml +++ b/doctork/config/log4j2.dev.xml @@ -4,14 +4,14 @@ - + - + diff --git a/drkafka/config/log4j2.xml b/doctork/config/log4j2.xml similarity index 75% rename from drkafka/config/log4j2.xml rename to doctork/config/log4j2.xml index 55b160e6..5776893f 100644 --- a/drkafka/config/log4j2.xml +++ b/doctork/config/log4j2.xml @@ -4,14 +4,14 @@ - + - + diff --git a/drkafka/pom.xml b/doctork/pom.xml similarity index 97% rename from drkafka/pom.xml rename to doctork/pom.xml index 67d3cb50..29446b07 100644 --- a/drkafka/pom.xml +++ b/doctork/pom.xml @@ -3,15 +3,15 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - doctorkafka-parent + doctork-parent com.github.pinterest - 0.2.4.9 + 0.2.4.10 ../pom.xml 4.0.0 - doctorkafka + doctork Kafka cluster healing and workload balancing - doctorkafka + doctork 1.3.15 @@ -220,7 +220,7 @@ - src/main/assembly/doctorkafka.xml + src/main/assembly/doctork.xml false diff --git a/doctork/scripts/action_retriever.sh b/doctork/scripts/action_retriever.sh new file mode 100755 index 00000000..fad4b07f --- /dev/null +++ b/doctork/scripts/action_retriever.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -x + +java -cp doctork/target/lib/*:doctork/target/doctork-0.2.4.10.jar \ + -Dlog4j.configurationFile=file:./doctork/config/log4j2.dev.xml \ + com.pinterest.doctork.tools.DoctorKActionRetriever \ + $1 $2 $3 $4 $5 $6 diff --git a/drkafka/scripts/run.sh b/doctork/scripts/run.sh similarity index 62% rename from drkafka/scripts/run.sh rename to doctork/scripts/run.sh index 02a9ae0f..8cd727db 100755 --- a/drkafka/scripts/run.sh +++ b/doctork/scripts/run.sh @@ -3,4 +3,4 @@ set -x java8 -cp lib/*:kafkaoperator-0.2.3.jar -Dlog4j.configurationFile=file:./log4j2.xml \ - com.pinterest.doctorkafka.DoctorKafkaMain server config.yaml \ No newline at end of file + com.pinterest.doctork.DoctorKMain server config.yaml \ No newline at end of file diff --git a/doctork/scripts/run_dev.sh b/doctork/scripts/run_dev.sh new file mode 100755 index 00000000..f9d29aaf --- /dev/null +++ b/doctork/scripts/run_dev.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -x + +java -cp doctork/target/lib/*:doctork/target/doctork-0.2.4.10.jar \ + -Dlog4j.configurationFile=file:./doctork/config/log4j2.dev.xml \ + com.pinterest.doctork.DoctorKMain server doctork/config/doctork.dev.yaml \ No newline at end of file diff --git a/drkafka/scripts/run_in_container.sh b/doctork/scripts/run_in_container.sh similarity index 87% rename from drkafka/scripts/run_in_container.sh rename to doctork/scripts/run_in_container.sh index e58b6e4d..8e83bd21 100755 --- a/drkafka/scripts/run_in_container.sh +++ b/doctork/scripts/run_in_container.sh @@ -5,9 +5,9 @@ ulimit -n 65536 export SERVICENAME=${SERVICENAME:=kafkaoperator} -export JAVA_MAIN=${JAVA_MAIN:=com.pinterest.doctorkafka.DoctorKafkaMain} -export LOG4J_CONFIG_FILE=${LOG4J_CONFIG_FILE:=/opt/doctorkafka/log4j2.xml} -export CONFIG_YAML=${CONFIG_FILE:=drkafka/config/doctorkafka.docker.yaml} +export JAVA_MAIN=${JAVA_MAIN:=com.pinterest.doctork.DoctorKMain} +export LOG4J_CONFIG_FILE=${LOG4J_CONFIG_FILE:=/opt/doctork/log4j2.xml} +export CONFIG_YAML=${CONFIG_FILE:=doctork/config/doctork.docker.yaml} HEAP_SIZE=${HEAP_SIZE:=2G} NEW_SIZE=${NEW_SIZE:=1G} diff --git a/doctork/scripts/run_prod.sh b/doctork/scripts/run_prod.sh new file mode 100755 index 00000000..cfa82f3f --- /dev/null +++ b/doctork/scripts/run_prod.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -x + +java -cp doctork/target/lib/*:doctork/target/doctork-0.1.0.jar \ + -Dlog4j.configurationFile=file:./doctork/config/log4j2.xml \ + com.pinterest.doctork.DoctorKMain \ + server doctork/config/doctork.prod.yaml \ No newline at end of file diff --git a/drkafka/src/main/assembly/doctorkafka.xml b/doctork/src/main/assembly/doctork.xml similarity index 100% rename from drkafka/src/main/assembly/doctorkafka.xml rename to doctork/src/main/assembly/doctork.xml diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/DoctorKafka.java b/doctork/src/main/java/com/pinterest/doctork/DoctorK.java similarity index 57% rename from drkafka/src/main/java/com/pinterest/doctorkafka/DoctorKafka.java rename to doctork/src/main/java/com/pinterest/doctork/DoctorK.java index 271d3638..0a115628 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/DoctorKafka.java +++ b/doctork/src/main/java/com/pinterest/doctork/DoctorK.java @@ -1,10 +1,10 @@ -package com.pinterest.doctorkafka; +package com.pinterest.doctork; -import com.pinterest.doctorkafka.config.DoctorKafkaClusterConfig; -import com.pinterest.doctorkafka.config.DoctorKafkaConfig; -import com.pinterest.doctorkafka.replicastats.BrokerStatsProcessor; -import com.pinterest.doctorkafka.replicastats.ReplicaStatsManager; -import com.pinterest.doctorkafka.util.ZookeeperClient; +import com.pinterest.doctork.config.DoctorKClusterConfig; +import com.pinterest.doctork.config.DoctorKConfig; +import com.pinterest.doctork.replicastats.BrokerStatsProcessor; +import com.pinterest.doctork.replicastats.ReplicaStatsManager; +import com.pinterest.doctork.util.ZookeeperClient; import org.apache.kafka.common.security.auth.SecurityProtocol; import org.apache.logging.log4j.LogManager; @@ -17,15 +17,15 @@ import java.util.Map; import java.util.Set; -public class DoctorKafka { +public class DoctorK { - private static final Logger LOG = LogManager.getLogger(DoctorKafka.class); + private static final Logger LOG = LogManager.getLogger(DoctorK.class); - private DoctorKafkaConfig drkafkaConf; + private DoctorKConfig doctorKConf; private BrokerStatsProcessor brokerStatsProcessor = null; - private DoctorKafkaActionReporter actionReporter = null; + private DoctorKActionReporter actionReporter = null; private Map clusterManagers = new HashMap<>(); @@ -33,38 +33,38 @@ public class DoctorKafka { private ZookeeperClient zookeeperClient = null; - private DoctorKafkaHeartbeat heartbeat = null; + private DoctorKHeartbeat heartbeat = null; private ReplicaStatsManager replicaStatsManager = null; - public DoctorKafka(ReplicaStatsManager replicaStatsManager) { + public DoctorK(ReplicaStatsManager replicaStatsManager) { this.replicaStatsManager = replicaStatsManager; - this.drkafkaConf = replicaStatsManager.getConfig(); - this.clusterZkUrls = drkafkaConf.getClusterZkUrls(); - this.zookeeperClient = new ZookeeperClient(drkafkaConf.getDoctorKafkaZkurl()); + this.doctorKConf = replicaStatsManager.getConfig(); + this.clusterZkUrls = doctorKConf.getClusterZkUrls(); + this.zookeeperClient = new ZookeeperClient(doctorKConf.getDoctorKZkurl()); } public void start() { - String brokerstatsZkurl = drkafkaConf.getBrokerstatsZkurl(); - String actionReportZkurl = drkafkaConf.getActionReportZkurl(); - String statsTopic = drkafkaConf.getBrokerStatsTopic(); - SecurityProtocol statsSecurityProtocol = drkafkaConf.getBrokerStatsConsumerSecurityProtocol(); - String actionReportTopic = drkafkaConf.getActionReportTopic(); - SecurityProtocol actionReportSecurityProtocol = drkafkaConf.getActionReportProducerSecurityProtocol(); + String brokerstatsZkurl = doctorKConf.getBrokerstatsZkurl(); + String actionReportZkurl = doctorKConf.getActionReportZkurl(); + String statsTopic = doctorKConf.getBrokerStatsTopic(); + SecurityProtocol statsSecurityProtocol = doctorKConf.getBrokerStatsConsumerSecurityProtocol(); + String actionReportTopic = doctorKConf.getActionReportTopic(); + SecurityProtocol actionReportSecurityProtocol = doctorKConf.getActionReportProducerSecurityProtocol(); LOG.info("Start rebuilding the replica stats by reading the past 24 hours brokerstats"); replicaStatsManager.readPastReplicaStats(brokerstatsZkurl, statsSecurityProtocol, - drkafkaConf.getBrokerStatsTopic(), drkafkaConf.getBrokerStatsBacktrackWindowsInSeconds()); + doctorKConf.getBrokerStatsTopic(), doctorKConf.getBrokerStatsBacktrackWindowsInSeconds()); LOG.info("Finish rebuilding the replica stats"); brokerStatsProcessor = new BrokerStatsProcessor(brokerstatsZkurl, statsSecurityProtocol, statsTopic, - drkafkaConf.getBrokerStatsConsumerSslConfigs(), replicaStatsManager); + doctorKConf.getBrokerStatsConsumerSslConfigs(), replicaStatsManager); brokerStatsProcessor.start(); - actionReporter = new DoctorKafkaActionReporter(actionReportZkurl, actionReportSecurityProtocol, actionReportTopic, - drkafkaConf.getActionReportProducerSslConfigs()); + actionReporter = new DoctorKActionReporter(actionReportZkurl, actionReportSecurityProtocol, actionReportTopic, + doctorKConf.getActionReportProducerSslConfigs()); for (String clusterZkUrl : clusterZkUrls) { - DoctorKafkaClusterConfig clusterConf = drkafkaConf.getClusterConfigByZkUrl(clusterZkUrl); + DoctorKClusterConfig clusterConf = doctorKConf.getClusterConfigByZkUrl(clusterZkUrl); KafkaCluster kafkaCluster = replicaStatsManager.getClusters().get(clusterZkUrl); if (kafkaCluster == null) { @@ -72,13 +72,13 @@ public void start() { continue; } KafkaClusterManager clusterManager = new KafkaClusterManager( - clusterZkUrl, kafkaCluster, clusterConf, drkafkaConf, actionReporter, zookeeperClient, replicaStatsManager); + clusterZkUrl, kafkaCluster, clusterConf, doctorKConf, actionReporter, zookeeperClient, replicaStatsManager); clusterManagers.put(clusterConf.getClusterName(), clusterManager); clusterManager.start(); LOG.info("Starting cluster manager for " + clusterZkUrl); } - heartbeat = new DoctorKafkaHeartbeat(); + heartbeat = new DoctorKHeartbeat(); heartbeat.start(); } @@ -91,8 +91,8 @@ public void stop() { } } - public DoctorKafkaConfig getDoctorKafkaConfig() { - return drkafkaConf; + public DoctorKConfig getDoctorKConfig() { + return doctorKConf; } public Collection getClusterManagers() { diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/DoctorKafkaActionReporter.java b/doctork/src/main/java/com/pinterest/doctork/DoctorKActionReporter.java similarity index 91% rename from drkafka/src/main/java/com/pinterest/doctorkafka/DoctorKafkaActionReporter.java rename to doctork/src/main/java/com/pinterest/doctork/DoctorKActionReporter.java index c9e1226d..78d7e588 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/DoctorKafkaActionReporter.java +++ b/doctork/src/main/java/com/pinterest/doctork/DoctorKActionReporter.java @@ -1,6 +1,6 @@ -package com.pinterest.doctorkafka; +package com.pinterest.doctork; -import com.pinterest.doctorkafka.util.OperatorUtil; +import com.pinterest.doctork.util.OperatorUtil; import java.util.Map; import org.apache.avro.io.BinaryEncoder; @@ -20,9 +20,9 @@ import java.util.Properties; import java.util.concurrent.Future; -public class DoctorKafkaActionReporter { +public class DoctorKActionReporter { - private static final Logger LOG = LogManager.getLogger(DoctorKafkaActionReporter.class); + private static final Logger LOG = LogManager.getLogger(DoctorKActionReporter.class); private static final int MAX_RETRIES = 5; private static final EncoderFactory avroEncoderFactory = EncoderFactory.get(); private static final SpecificDatumWriter avroWriter @@ -31,8 +31,8 @@ public class DoctorKafkaActionReporter { private String topic; private final Producer kafkaProducer; - public DoctorKafkaActionReporter(String zkUrl, SecurityProtocol securityProtocol, - String topic, Map producerConfigs) { + public DoctorKActionReporter(String zkUrl, SecurityProtocol securityProtocol, + String topic, Map producerConfigs) { this.topic = topic; String bootstrapBrokers = OperatorUtil.getBrokers(zkUrl, securityProtocol); Properties props = new Properties(); diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/DoctorKafkaHeartbeat.java b/doctork/src/main/java/com/pinterest/doctork/DoctorKHeartbeat.java similarity index 72% rename from drkafka/src/main/java/com/pinterest/doctorkafka/DoctorKafkaHeartbeat.java rename to doctork/src/main/java/com/pinterest/doctork/DoctorKHeartbeat.java index fc97a89e..a56931a4 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/DoctorKafkaHeartbeat.java +++ b/doctork/src/main/java/com/pinterest/doctork/DoctorKHeartbeat.java @@ -1,6 +1,6 @@ -package com.pinterest.doctorkafka; +package com.pinterest.doctork; -import com.pinterest.doctorkafka.util.OpenTsdbMetricConverter; +import com.pinterest.doctork.util.OpenTsdbMetricConverter; import com.google.common.util.concurrent.ThreadFactoryBuilder; @@ -8,11 +8,11 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -public class DoctorKafkaHeartbeat implements Runnable { +public class DoctorKHeartbeat implements Runnable { private static final int HEARTBEAT_INTERVAL_IN_SECONDS = 60; public ScheduledExecutorService heartbeatExecutor; - DoctorKafkaHeartbeat() { + DoctorKHeartbeat() { heartbeatExecutor = Executors.newSingleThreadScheduledExecutor( new ThreadFactoryBuilder().setNameFormat("Heartbeat").build()); } @@ -27,6 +27,6 @@ public void stop() { @Override public void run() { - OpenTsdbMetricConverter.gauge(DoctorKafkaMetrics.DOCTORKAFKA_SERVICE_RUNNING, 1.0); + OpenTsdbMetricConverter.gauge(DoctorKMetrics.DOCTORK_SERVICE_RUNNING, 1.0); } } diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/DoctorKafkaMain.java b/doctork/src/main/java/com/pinterest/doctork/DoctorKMain.java similarity index 60% rename from drkafka/src/main/java/com/pinterest/doctorkafka/DoctorKafkaMain.java rename to doctork/src/main/java/com/pinterest/doctork/DoctorKMain.java index 9572af0b..d882e603 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/DoctorKafkaMain.java +++ b/doctork/src/main/java/com/pinterest/doctork/DoctorKMain.java @@ -1,30 +1,30 @@ -package com.pinterest.doctorkafka; +package com.pinterest.doctork; import java.util.Collections; import java.util.NoSuchElementException; import java.util.concurrent.Executors; +import com.pinterest.doctork.security.DoctorKAuthorizationFilter; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature; import com.google.common.collect.ImmutableList; -import com.pinterest.doctorkafka.api.BrokersDecommissionApi; -import com.pinterest.doctorkafka.api.ClustersMaintenanceApi; -import com.pinterest.doctorkafka.api.BrokersApi; -import com.pinterest.doctorkafka.api.ClustersApi; -import com.pinterest.doctorkafka.config.DoctorKafkaAppConfig; -import com.pinterest.doctorkafka.config.DoctorKafkaConfig; -import com.pinterest.doctorkafka.replicastats.ReplicaStatsManager; -import com.pinterest.doctorkafka.security.DrKafkaAuthorizationFilter; -import com.pinterest.doctorkafka.servlet.ClusterInfoServlet; -import com.pinterest.doctorkafka.servlet.DoctorKafkaActionsServlet; -import com.pinterest.doctorkafka.servlet.DoctorKafkaBrokerStatsServlet; -import com.pinterest.doctorkafka.servlet.DoctorKafkaInfoServlet; -import com.pinterest.doctorkafka.servlet.KafkaTopicStatsServlet; -import com.pinterest.doctorkafka.servlet.UnderReplicatedPartitionsServlet; -import com.pinterest.doctorkafka.util.OperatorUtil; +import com.pinterest.doctork.api.BrokersDecommissionApi; +import com.pinterest.doctork.api.ClustersMaintenanceApi; +import com.pinterest.doctork.api.BrokersApi; +import com.pinterest.doctork.api.ClustersApi; +import com.pinterest.doctork.config.DoctorKAppConfig; +import com.pinterest.doctork.config.DoctorKConfig; +import com.pinterest.doctork.replicastats.ReplicaStatsManager; +import com.pinterest.doctork.servlet.ClusterInfoServlet; +import com.pinterest.doctork.servlet.DoctorKActionsServlet; +import com.pinterest.doctork.servlet.DoctorKBrokerStatsServlet; +import com.pinterest.doctork.servlet.DoctorKInfoServlet; +import com.pinterest.doctork.servlet.KafkaTopicStatsServlet; +import com.pinterest.doctork.servlet.UnderReplicatedPartitionsServlet; +import com.pinterest.doctork.util.OperatorUtil; import io.dropwizard.Application; import io.dropwizard.assets.AssetsBundle; @@ -36,57 +36,57 @@ import io.dropwizard.setup.Environment; /** - * DoctorKafka is the central service for managing kafka operation. + * DoctorK is the central service for managing kafka operation. * */ -public class DoctorKafkaMain extends Application { +public class DoctorKMain extends Application { - private static final Logger LOG = LogManager.getLogger(DoctorKafkaMain.class); + private static final Logger LOG = LogManager.getLogger(DoctorKMain.class); private static final String OSTRICH_PORT = "ostrichport"; - public static DoctorKafka doctorKafka = null; - private static DoctorKafkaWatcher operatorWatcher = null; + public static DoctorK doctorK = null; + private static DoctorKWatcher operatorWatcher = null; public static ReplicaStatsManager replicaStatsManager = null; @Override - public void initialize(Bootstrap bootstrap) { + public void initialize(Bootstrap bootstrap) { bootstrap.addBundle(new AssetsBundle("/webapp/pages/", "/", "index.html")); } @Override - public void run(DoctorKafkaAppConfig configuration, Environment environment) throws Exception { - Runtime.getRuntime().addShutdownHook(new DoctorKafkaMain.OperatorCleanupThread()); + public void run(DoctorKAppConfig configuration, Environment environment) throws Exception { + Runtime.getRuntime().addShutdownHook(new DoctorKMain.OperatorCleanupThread()); LOG.info("Configuration path : {}", configuration.getConfig()); - replicaStatsManager = new ReplicaStatsManager(new DoctorKafkaConfig(configuration.getConfig())); + replicaStatsManager = new ReplicaStatsManager(new DoctorKConfig(configuration.getConfig())); if (!replicaStatsManager.getConfig().getRestartDisabled()){ - operatorWatcher = new DoctorKafkaWatcher(replicaStatsManager.getConfig().getRestartIntervalInSeconds()); + operatorWatcher = new DoctorKWatcher(replicaStatsManager.getConfig().getRestartIntervalInSeconds()); operatorWatcher.start(); } configureServerRuntime(configuration, replicaStatsManager.getConfig()); - doctorKafka = new DoctorKafka(replicaStatsManager); + doctorK = new DoctorK(replicaStatsManager); - registerAPIs(environment, doctorKafka, replicaStatsManager.getConfig()); + registerAPIs(environment, doctorK, replicaStatsManager.getConfig()); registerServlets(environment); Executors.newCachedThreadPool().submit(() -> { try { - doctorKafka.start(); - LOG.info("DoctorKafka started"); + doctorK.start(); + LOG.info("DoctorK started"); } catch (Exception e) { - LOG.error("DoctorKafka start failed", e); + LOG.error("DoctorK start failed", e); } }); startMetricsService(); - LOG.info("DoctorKafka API server started"); + LOG.info("DoctorK API server started"); } - private void configureServerRuntime(DoctorKafkaAppConfig configuration, DoctorKafkaConfig config) { + private void configureServerRuntime(DoctorKAppConfig configuration, DoctorKConfig config) { DefaultServerFactory defaultServerFactory = (DefaultServerFactory) configuration.getServerFactory(); @@ -117,28 +117,28 @@ private void configureServerRuntime(DoctorKafkaAppConfig configuration, DoctorKa defaultServerFactory.setApplicationConnectors(Collections.singletonList(application)); } - private void registerAPIs(Environment environment, DoctorKafka doctorKafka, DoctorKafkaConfig doctorKafkaConfig) { + private void registerAPIs(Environment environment, DoctorK doctorK, DoctorKConfig doctorKConfig) { environment.jersey().setUrlPattern("/api/*"); - checkAndInitializeAuthorizationFilter(environment, doctorKafkaConfig); - environment.jersey().register(new BrokersApi(doctorKafka)); - environment.jersey().register(new ClustersApi(doctorKafka)); - environment.jersey().register(new ClustersMaintenanceApi(doctorKafka)); - environment.jersey().register(new BrokersDecommissionApi(doctorKafka)); + checkAndInitializeAuthorizationFilter(environment, doctorKConfig); + environment.jersey().register(new BrokersApi(doctorK)); + environment.jersey().register(new ClustersApi(doctorK)); + environment.jersey().register(new ClustersMaintenanceApi(doctorK)); + environment.jersey().register(new BrokersDecommissionApi(doctorK)); } - private void checkAndInitializeAuthorizationFilter(Environment environment, DoctorKafkaConfig doctorKafkaConfig) { + private void checkAndInitializeAuthorizationFilter(Environment environment, DoctorKConfig doctorKConfig) { LOG.info("Checking authorization filter"); try { - Class authorizationFilterClass = doctorKafkaConfig.getAuthorizationFilterClass(); + Class authorizationFilterClass = doctorKConfig.getAuthorizationFilterClass(); if (authorizationFilterClass != null) { - DrKafkaAuthorizationFilter filter = authorizationFilterClass.newInstance(); - filter.configure(doctorKafkaConfig); + DoctorKAuthorizationFilter filter = authorizationFilterClass.newInstance(); + filter.configure(doctorKConfig); LOG.info("Using authorization filer:" + filter.getClass().getName()); environment.jersey().register(filter); environment.jersey().register(RolesAllowedDynamicFeature.class); } } catch (Exception e) { - LOG.error("Failed to get and initialize DrKafkaAuthorizationFilter", e); + LOG.error("Failed to get and initialize DoctorKAuthorizationFilter", e); } } @@ -151,7 +151,7 @@ private void startMetricsService() { throw new NoSuchElementException( String.format("Key '%s' does not map to an existing object!", OSTRICH_PORT)); } else { - OperatorUtil.startOstrichService("doctorkafka", tsdHostPort, ostrichPort); + OperatorUtil.startOstrichService("doctork", tsdHostPort, ostrichPort); } } @@ -160,17 +160,17 @@ private void registerServlets(Environment environment) { "/servlet/clusterinfo"); environment.getApplicationContext().addServlet(KafkaTopicStatsServlet.class, "/servlet/topicstats"); - environment.getApplicationContext().addServlet(DoctorKafkaActionsServlet.class, + environment.getApplicationContext().addServlet(DoctorKActionsServlet.class, "/servlet/actions"); - environment.getApplicationContext().addServlet(DoctorKafkaInfoServlet.class, "/servlet/info"); - environment.getApplicationContext().addServlet(DoctorKafkaBrokerStatsServlet.class, + environment.getApplicationContext().addServlet(DoctorKInfoServlet.class, "/servlet/info"); + environment.getApplicationContext().addServlet(DoctorKBrokerStatsServlet.class, "/servlet/brokerstats"); environment.getApplicationContext().addServlet(UnderReplicatedPartitionsServlet.class, "/servlet/urp"); } public static void main(String[] args) throws Exception { - new DoctorKafkaMain().run(args); + new DoctorKMain().run(args); } static class OperatorCleanupThread extends Thread { @@ -178,8 +178,8 @@ static class OperatorCleanupThread extends Thread { @Override public void run() { try { - if (doctorKafka != null) { - doctorKafka.stop(); + if (doctorK != null) { + doctorK.stop(); } } catch (Throwable t) { LOG.error("Failure in stopping operator", t); diff --git a/doctork/src/main/java/com/pinterest/doctork/DoctorKMetrics.java b/doctork/src/main/java/com/pinterest/doctork/DoctorKMetrics.java new file mode 100644 index 00000000..7c63270b --- /dev/null +++ b/doctork/src/main/java/com/pinterest/doctork/DoctorKMetrics.java @@ -0,0 +1,9 @@ +package com.pinterest.doctork; + +public class DoctorKMetrics { + + public static final String HANDLE_URP_FAILURE = "doctork.failure.handle_under_replication"; + public static final String BROKERSTATS_MESSAGES = "doctork.brokerstats.messages"; + public static final String DOCTORK_SERVICE_RUNNING = "doctork.service.running"; + public static final String MESSAGE_DESERIALIZE_ERROR = "doctork.message.decode.error"; +} diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/DoctorKafkaWatcher.java b/doctork/src/main/java/com/pinterest/doctork/DoctorKWatcher.java similarity index 87% rename from drkafka/src/main/java/com/pinterest/doctorkafka/DoctorKafkaWatcher.java rename to doctork/src/main/java/com/pinterest/doctork/DoctorKWatcher.java index 1eb7691e..58e16570 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/DoctorKafkaWatcher.java +++ b/doctork/src/main/java/com/pinterest/doctork/DoctorKWatcher.java @@ -1,4 +1,4 @@ -package com.pinterest.doctorkafka; +package com.pinterest.doctork; import com.google.common.util.concurrent.ThreadFactoryBuilder; import org.apache.logging.log4j.LogManager; @@ -8,8 +8,8 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -public class DoctorKafkaWatcher implements Runnable { - private static final Logger LOG = LogManager.getLogger(DoctorKafkaWatcher.class); +public class DoctorKWatcher implements Runnable { + private static final Logger LOG = LogManager.getLogger(DoctorKWatcher.class); private static final int INITIAL_DELAY = 0; /** * The executor service for executing MercedWorkerMonitor thread @@ -23,7 +23,7 @@ public class DoctorKafkaWatcher implements Runnable { private long restartTime; - public DoctorKafkaWatcher(long uptimeInSeconds) { + public DoctorKWatcher(long uptimeInSeconds) { this.restartTime = System.currentTimeMillis() + uptimeInSeconds * 1000L; } diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/KafkaBroker.java b/doctork/src/main/java/com/pinterest/doctork/KafkaBroker.java similarity index 97% rename from drkafka/src/main/java/com/pinterest/doctorkafka/KafkaBroker.java rename to doctork/src/main/java/com/pinterest/doctork/KafkaBroker.java index 06227263..5e25052d 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/KafkaBroker.java +++ b/doctork/src/main/java/com/pinterest/doctork/KafkaBroker.java @@ -1,6 +1,6 @@ -package com.pinterest.doctorkafka; +package com.pinterest.doctork; -import com.pinterest.doctorkafka.config.DoctorKafkaClusterConfig; +import com.pinterest.doctork.config.DoctorKClusterConfig; import com.google.common.annotations.VisibleForTesting; import org.apache.kafka.common.TopicPartition; @@ -25,7 +25,7 @@ public class KafkaBroker implements Comparable { private static final Logger LOG = LogManager.getLogger(KafkaBroker.class); private static final Gson gson = new Gson(); - private DoctorKafkaClusterConfig clusterConfig; + private DoctorKClusterConfig clusterConfig; private String zkUrl; private int brokerId; private String brokerName; @@ -47,7 +47,7 @@ public class KafkaBroker implements Comparable { private KafkaCluster kafkaCluster; private AtomicBoolean isDecommissioned = new AtomicBoolean(false); - public KafkaBroker(DoctorKafkaClusterConfig clusterConfig, KafkaCluster kafkaCluster, int brokerId) { + public KafkaBroker(DoctorKClusterConfig clusterConfig, KafkaCluster kafkaCluster, int brokerId) { assert clusterConfig != null; this.zkUrl = clusterConfig.getZkUrl(); this.brokerId = brokerId; diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/KafkaCluster.java b/doctork/src/main/java/com/pinterest/doctork/KafkaCluster.java similarity index 98% rename from drkafka/src/main/java/com/pinterest/doctorkafka/KafkaCluster.java rename to doctork/src/main/java/com/pinterest/doctork/KafkaCluster.java index 2939235a..f4441baa 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/KafkaCluster.java +++ b/doctork/src/main/java/com/pinterest/doctork/KafkaCluster.java @@ -1,8 +1,8 @@ -package com.pinterest.doctorkafka; +package com.pinterest.doctork; -import com.pinterest.doctorkafka.config.DoctorKafkaClusterConfig; -import com.pinterest.doctorkafka.util.OutOfSyncReplica; +import com.pinterest.doctork.config.DoctorKClusterConfig; +import com.pinterest.doctork.util.OutOfSyncReplica; import com.codahale.metrics.Histogram; import com.codahale.metrics.SlidingWindowReservoir; @@ -51,7 +51,7 @@ public class KafkaCluster { private static final long REASSIGNMENT_COOLDOWN_WINDOW_IN_MS = 1800 * 1000L; private static final int SLIDING_WINDOW_SIZE = 1440 * 4; - private DoctorKafkaClusterConfig clusterConfig; + private DoctorKClusterConfig clusterConfig; public String zkUrl; public ConcurrentMap brokers; private ConcurrentMap> brokerStatsMap; @@ -60,7 +60,7 @@ public class KafkaCluster { private ConcurrentMap bytesOutHistograms = new ConcurrentHashMap<>(); private ConcurrentMap reassignmentTimestamps = new ConcurrentHashMap<>(); - public KafkaCluster(String zookeeper, DoctorKafkaClusterConfig clusterConfig) { + public KafkaCluster(String zookeeper, DoctorKClusterConfig clusterConfig) { this.zkUrl = zookeeper; this.brokers = new ConcurrentHashMap<>(); this.clusterConfig = clusterConfig; diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/KafkaClusterManager.java b/doctork/src/main/java/com/pinterest/doctork/KafkaClusterManager.java similarity index 95% rename from drkafka/src/main/java/com/pinterest/doctorkafka/KafkaClusterManager.java rename to doctork/src/main/java/com/pinterest/doctork/KafkaClusterManager.java index c775cf94..45d16508 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/KafkaClusterManager.java +++ b/doctork/src/main/java/com/pinterest/doctork/KafkaClusterManager.java @@ -1,18 +1,18 @@ -package com.pinterest.doctorkafka; - -import com.pinterest.doctorkafka.config.DoctorKafkaClusterConfig; -import com.pinterest.doctorkafka.config.DoctorKafkaConfig; -import com.pinterest.doctorkafka.notification.Email; -import com.pinterest.doctorkafka.replicastats.ReplicaStatsManager; -import com.pinterest.doctorkafka.util.BrokerReplacer; -import com.pinterest.doctorkafka.util.KafkaUtils; -import com.pinterest.doctorkafka.util.OpenTsdbMetricConverter; -import com.pinterest.doctorkafka.util.OperatorUtil; -import com.pinterest.doctorkafka.util.OutOfSyncReplica; -import com.pinterest.doctorkafka.util.PreferredReplicaElectionInfo; -import com.pinterest.doctorkafka.util.ReassignmentInfo; -import com.pinterest.doctorkafka.util.UnderReplicatedReason; -import com.pinterest.doctorkafka.util.ZookeeperClient; +package com.pinterest.doctork; + +import com.pinterest.doctork.config.DoctorKClusterConfig; +import com.pinterest.doctork.config.DoctorKConfig; +import com.pinterest.doctork.notification.Email; +import com.pinterest.doctork.replicastats.ReplicaStatsManager; +import com.pinterest.doctork.util.BrokerReplacer; +import com.pinterest.doctork.util.KafkaUtils; +import com.pinterest.doctork.util.OpenTsdbMetricConverter; +import com.pinterest.doctork.util.OperatorUtil; +import com.pinterest.doctork.util.OutOfSyncReplica; +import com.pinterest.doctork.util.PreferredReplicaElectionInfo; +import com.pinterest.doctork.util.ReassignmentInfo; +import com.pinterest.doctork.util.UnderReplicatedReason; +import com.pinterest.doctork.util.ZookeeperClient; import kafka.cluster.Broker; import kafka.common.TopicAndPartition; @@ -74,9 +74,9 @@ public class KafkaClusterManager implements Runnable { private Map consumerConfigs; private ZkUtils zkUtils; private KafkaCluster kafkaCluster = null; - private DoctorKafkaConfig drkafkaConfig = null; - private DoctorKafkaClusterConfig clusterConfig; - private DoctorKafkaActionReporter actionReporter = null; + private DoctorKConfig doctorKConfig = null; + private DoctorKClusterConfig clusterConfig; + private DoctorKActionReporter actionReporter = null; private boolean stopped = true; private Thread thread = null; @@ -97,9 +97,9 @@ public class KafkaClusterManager implements Runnable { private AtomicBoolean maintenanceMode = new AtomicBoolean(false); public KafkaClusterManager(String zkUrl, KafkaCluster kafkaCluster, - DoctorKafkaClusterConfig clusterConfig, - DoctorKafkaConfig drkafkaConfig, - DoctorKafkaActionReporter actionReporter, + DoctorKClusterConfig clusterConfig, + DoctorKConfig doctorKConfig, + DoctorKActionReporter actionReporter, ZookeeperClient zookeeperClient, ReplicaStatsManager replicaStatsManager) { assert clusterConfig != null; @@ -109,13 +109,13 @@ public KafkaClusterManager(String zkUrl, KafkaCluster kafkaCluster, this.consumerConfigs = clusterConfig.getConsumerConfigurations(); this.kafkaCluster = kafkaCluster; this.clusterConfig = clusterConfig; - this.drkafkaConfig = drkafkaConfig; + this.doctorKConfig = doctorKConfig; this.actionReporter = actionReporter; this.bytesInLimit = clusterConfig.getNetworkInLimitInBytes(); this.bytesOutLimit = clusterConfig.getNetworkOutLimitInBytes(); this.zookeeperClient = zookeeperClient; if (clusterConfig.enabledDeadbrokerReplacement()) { - this.brokerReplacer = new BrokerReplacer(drkafkaConfig.getBrokerReplacementCommand()); + this.brokerReplacer = new BrokerReplacer(doctorKConfig.getBrokerReplacementCommand()); } } @@ -508,7 +508,7 @@ private void reassignTopicPartitions(String jsonReassignmentData) { LOG.info("Set the reassignment data: "); actionReporter.sendMessage(clusterConfig.getClusterName(), "partition reassignment : " + jsonReassignmentData); - Email.notifyOnPartitionReassignment(drkafkaConfig.getNotificationEmails(), + Email.notifyOnPartitionReassignment(doctorKConfig.getNotificationEmails(), clusterConfig.getClusterName(), jsonReassignmentData); } } @@ -752,12 +752,12 @@ public void handleUnderReplicatedPartitions(List initialUrps, alertOnFailure = false; } else { LOG.error("Failed to generate a reassignment plan"); - OpenTsdbMetricConverter.incr(DoctorKafkaMetrics.HANDLE_URP_FAILURE, 1, "cluster=" + zkUrl); + OpenTsdbMetricConverter.incr(DoctorKMetrics.HANDLE_URP_FAILURE, 1, "cluster=" + zkUrl); } } if (alertOnFailure) { - Email.alertOnFailureInHandlingUrps(drkafkaConfig.getNotificationEmails(), + Email.alertOnFailureInHandlingUrps(doctorKConfig.getNotificationEmails(), clusterConfig.getClusterName(), urps, reassignmentFailures, downBrokers); } } @@ -790,7 +790,7 @@ private boolean isDeadBroker(String host, int kafkaPort, int brokerId, TopicPart return uptime < MAX_HOST_REBOOT_TIME_MS; } - // If a healthy broker is terminated before it can report any failure to doctorkafka, + // If a healthy broker is terminated before it can report any failure to doctork, // the last few brokers stats that kafkaoperator received would always be healthy stats. // Because of this, we cannot rely on brokerstats.hasFailure field alone to tell if broker // has failure or not. @@ -931,7 +931,7 @@ private boolean checkAndReplaceDeadBrokers() { long brokerReplacementTimeInSeconds = (now - brokerReplacer.getReplacementStartTime()) / 1000; if (brokerReplacementTimeInSeconds > MAX_HOST_REPLACEMENT_TIME_SECONDS) { // send out alerts for prolonged broker replacement - Email.alertOnProlongedBrokerReplacement(drkafkaConfig.getNotificationEmails(), + Email.alertOnProlongedBrokerReplacement(doctorKConfig.getNotificationEmails(), clusterConfig.getClusterName(), brokerReplacer.getReplacedBroker(), brokerReplacementTimeInSeconds); } @@ -962,7 +962,7 @@ private boolean checkAndReplaceDeadBrokers() { long lastReplacementTime = zookeeperClient.getLastBrokerReplacementTime(clusterConfig.getClusterName()); long elaspedTimeInSeconds = (now - lastReplacementTime) / 1000; - if (elaspedTimeInSeconds < drkafkaConfig.getBrokerReplacementIntervalInSeconds()) { + if (elaspedTimeInSeconds < doctorKConfig.getBrokerReplacementIntervalInSeconds()) { LOG.info("Cannot replace {}:{} due to replace frequency limitation", clusterName, brokerName); return false; @@ -977,7 +977,7 @@ private boolean checkAndReplaceDeadBrokers() { brokerReplacer.replaceBroker(brokerName); zookeeperClient.recordBrokerTermination(clusterName, brokerName); actionReporter.sendMessage(clusterName, "broker replacement : " + brokerName); - Email.notifyOnBrokerReplacement(drkafkaConfig.getNotificationEmails(), + Email.notifyOnBrokerReplacement(doctorKConfig.getNotificationEmails(), clusterName, brokerName); } else { LOG.info("Dry run: Replacing {} in {}", brokerName, clusterName); @@ -990,14 +990,14 @@ private boolean checkAndReplaceDeadBrokers() { public void enableMaintenanceMode() { maintenanceMode.set(true); LOG.info("Enabled maintenace mode for:" + clusterConfig.getClusterName()); - Email.notifyOnMaintenanceMode(drkafkaConfig.getNotificationEmails(), + Email.notifyOnMaintenanceMode(doctorKConfig.getNotificationEmails(), clusterConfig.getClusterName(), maintenanceMode.get()); } public void disableMaintenanceMode() { maintenanceMode.set(false); LOG.info("Disabled maintenace mode for:" + clusterConfig.getClusterName()); - Email.notifyOnMaintenanceMode(drkafkaConfig.getNotificationEmails(), + Email.notifyOnMaintenanceMode(doctorKConfig.getNotificationEmails(), clusterConfig.getClusterName(), maintenanceMode.get()); } @@ -1006,7 +1006,7 @@ public void decommissionBroker(Integer brokerId) { // only notify if state changed if (prevState == false) { - Email.notifyOnDecommissioningBroker(drkafkaConfig.getNotificationEmails(), kafkaCluster.name(), String.valueOf(brokerId)); + Email.notifyOnDecommissioningBroker(doctorKConfig.getNotificationEmails(), kafkaCluster.name(), String.valueOf(brokerId)); } } @@ -1015,7 +1015,7 @@ public void cancelDecommissionBroker(Integer brokerId) { // only notify if state changed if (prevState == true) { - Email.notifyOnCancelledDecommissioningBroker(drkafkaConfig.getNotificationEmails(), kafkaCluster.name(), String.valueOf(brokerId)); + Email.notifyOnCancelledDecommissioningBroker(doctorKConfig.getNotificationEmails(), kafkaCluster.name(), String.valueOf(brokerId)); } } @@ -1047,7 +1047,7 @@ public void run() { List noStatsBrokers = getNoStatsBrokers(); if (!noStatsBrokers.isEmpty()) { Email.alertOnNoStatsBrokers( - drkafkaConfig.getAlertEmails(), clusterConfig.getClusterName(), noStatsBrokers); + doctorKConfig.getAlertEmails(), clusterConfig.getClusterName(), noStatsBrokers); continue; } @@ -1084,7 +1084,7 @@ public void run() { long underReplicatedTimeMills = System.currentTimeMillis() - firstSeenUrpsTimestamp; if (underReplicatedTimeMills > clusterConfig.getUnderReplicatedAlertTimeInMs()) { - Email.alertOnProlongedUnderReplicatedPartitions(drkafkaConfig.getAlertEmails(), + Email.alertOnProlongedUnderReplicatedPartitions(doctorKConfig.getAlertEmails(), clusterConfig.getClusterName(), clusterConfig.getUnderReplicatedAlertTimeInSeconds(), underReplicatedPartitions); diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/api/BrokersApi.java b/doctork/src/main/java/com/pinterest/doctork/api/BrokersApi.java similarity index 65% rename from drkafka/src/main/java/com/pinterest/doctorkafka/api/BrokersApi.java rename to doctork/src/main/java/com/pinterest/doctork/api/BrokersApi.java index 130bb764..e63f79fa 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/api/BrokersApi.java +++ b/doctork/src/main/java/com/pinterest/doctork/api/BrokersApi.java @@ -1,4 +1,4 @@ -package com.pinterest.doctorkafka.api; +package com.pinterest.doctork.api; import java.util.List; @@ -9,17 +9,17 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -import com.pinterest.doctorkafka.DoctorKafka; -import com.pinterest.doctorkafka.KafkaBroker; -import com.pinterest.doctorkafka.KafkaClusterManager; +import com.pinterest.doctork.DoctorK; +import com.pinterest.doctork.KafkaBroker; +import com.pinterest.doctork.KafkaClusterManager; @Path("/clusters/{clusterName}/brokers") @Produces({ MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_JSON }) -public class BrokersApi extends DoctorKafkaApi { +public class BrokersApi extends DoctorKApi { - public BrokersApi(DoctorKafka drkafka){ - super(drkafka); + public BrokersApi(DoctorK doctorK){ + super(doctorK); } @GET diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/api/BrokersDecommissionApi.java b/doctork/src/main/java/com/pinterest/doctork/api/BrokersDecommissionApi.java similarity index 80% rename from drkafka/src/main/java/com/pinterest/doctorkafka/api/BrokersDecommissionApi.java rename to doctork/src/main/java/com/pinterest/doctork/api/BrokersDecommissionApi.java index a3dcc447..f2499cae 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/api/BrokersDecommissionApi.java +++ b/doctork/src/main/java/com/pinterest/doctork/api/BrokersDecommissionApi.java @@ -1,8 +1,8 @@ -package com.pinterest.doctorkafka.api; +package com.pinterest.doctork.api; -import com.pinterest.doctorkafka.DoctorKafka; -import com.pinterest.doctorkafka.config.DoctorKafkaConfig; -import com.pinterest.doctorkafka.util.ApiUtils; +import com.pinterest.doctork.DoctorK; +import com.pinterest.doctork.config.DoctorKConfig; +import com.pinterest.doctork.util.ApiUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -22,12 +22,12 @@ @Path("/clusters/{clusterName}/brokers/{brokerId}/admin/decommission") @Produces({MediaType.APPLICATION_JSON }) @Consumes({MediaType.APPLICATION_JSON }) -public class BrokersDecommissionApi extends DoctorKafkaApi { +public class BrokersDecommissionApi extends DoctorKApi { private static final Logger LOG = LogManager.getLogger(BrokersDecommissionApi.class); - public BrokersDecommissionApi(DoctorKafka drkafka) { - super(drkafka); + public BrokersDecommissionApi(DoctorK doctorK) { + super(doctorK); } @GET @@ -36,7 +36,7 @@ public boolean isBrokerDecommissioned(@PathParam("clusterName") String clusterNa } @PUT - @RolesAllowed({ DoctorKafkaConfig.DRKAFKA_ADMIN_ROLE }) + @RolesAllowed({ DoctorKConfig.DOCTORK_ADMIN_ROLE}) public void decommissionBroker(@Context HttpServletRequest ctx, @PathParam("clusterName") String clusterName, @PathParam("brokerId") String brokerIdStr) { @@ -45,7 +45,7 @@ public void decommissionBroker(@Context HttpServletRequest ctx, } @DELETE - @RolesAllowed({ DoctorKafkaConfig.DRKAFKA_ADMIN_ROLE }) + @RolesAllowed({ DoctorKConfig.DOCTORK_ADMIN_ROLE}) public void cancelDecommissionBroker(@Context HttpServletRequest ctx, @PathParam("clusterName") String clusterName, @PathParam("brokerId") String brokerIdStr) { diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/api/ClustersApi.java b/doctork/src/main/java/com/pinterest/doctork/api/ClustersApi.java similarity index 57% rename from drkafka/src/main/java/com/pinterest/doctorkafka/api/ClustersApi.java rename to doctork/src/main/java/com/pinterest/doctork/api/ClustersApi.java index 09d3a995..72bbe4de 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/api/ClustersApi.java +++ b/doctork/src/main/java/com/pinterest/doctork/api/ClustersApi.java @@ -1,4 +1,4 @@ -package com.pinterest.doctorkafka.api; +package com.pinterest.doctork.api; import java.util.List; @@ -8,20 +8,20 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -import com.pinterest.doctorkafka.DoctorKafka; +import com.pinterest.doctork.DoctorK; @Path("/clusters") @Produces({ MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_JSON }) -public class ClustersApi extends DoctorKafkaApi { +public class ClustersApi extends DoctorKApi { - public ClustersApi(DoctorKafka drKafka) { - super(drKafka); + public ClustersApi(DoctorK doctorK) { + super(doctorK); } @GET public List getClusterNames() { - return getDrkafka().getClusterNames(); + return getDoctorK().getClusterNames(); } } diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/api/ClustersMaintenanceApi.java b/doctork/src/main/java/com/pinterest/doctork/api/ClustersMaintenanceApi.java similarity index 76% rename from drkafka/src/main/java/com/pinterest/doctorkafka/api/ClustersMaintenanceApi.java rename to doctork/src/main/java/com/pinterest/doctork/api/ClustersMaintenanceApi.java index 1fa4ae6d..beb11414 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/api/ClustersMaintenanceApi.java +++ b/doctork/src/main/java/com/pinterest/doctork/api/ClustersMaintenanceApi.java @@ -1,4 +1,4 @@ -package com.pinterest.doctorkafka.api; +package com.pinterest.doctork.api; import javax.annotation.security.RolesAllowed; import javax.servlet.http.HttpServletRequest; @@ -12,23 +12,23 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; +import com.pinterest.doctork.DoctorK; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import com.pinterest.doctorkafka.DoctorKafka; -import com.pinterest.doctorkafka.KafkaClusterManager; -import com.pinterest.doctorkafka.config.DoctorKafkaConfig; -import com.pinterest.doctorkafka.util.ApiUtils; +import com.pinterest.doctork.KafkaClusterManager; +import com.pinterest.doctork.config.DoctorKConfig; +import com.pinterest.doctork.util.ApiUtils; @Path("/clusters/{clusterName}/admin/maintenance") @Produces({ MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_JSON }) -public class ClustersMaintenanceApi extends DoctorKafkaApi { +public class ClustersMaintenanceApi extends DoctorKApi { private static final Logger LOG = LogManager.getLogger(ClustersMaintenanceApi.class); - public ClustersMaintenanceApi(DoctorKafka drKafka) { - super(drKafka); + public ClustersMaintenanceApi(DoctorK doctorK) { + super(doctorK); } @GET @@ -38,7 +38,7 @@ public boolean checkMaintenance(@PathParam("clusterName") String clusterName) { } @PUT - @RolesAllowed({ DoctorKafkaConfig.DRKAFKA_ADMIN_ROLE }) + @RolesAllowed({ DoctorKConfig.DOCTORK_ADMIN_ROLE}) public void enableMaintenance(@Context HttpServletRequest ctx, @PathParam("clusterName") String clusterName) { KafkaClusterManager clusterManager = checkAndGetClusterManager(clusterName); @@ -47,7 +47,7 @@ public void enableMaintenance(@Context HttpServletRequest ctx, } @DELETE - @RolesAllowed({ DoctorKafkaConfig.DRKAFKA_ADMIN_ROLE }) + @RolesAllowed({ DoctorKConfig.DOCTORK_ADMIN_ROLE}) public void disableMaintenance(@Context HttpServletRequest ctx, @PathParam("clusterName") String clusterName) { KafkaClusterManager clusterManager = checkAndGetClusterManager(clusterName); diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/api/DoctorKafkaApi.java b/doctork/src/main/java/com/pinterest/doctork/api/DoctorKApi.java similarity index 58% rename from drkafka/src/main/java/com/pinterest/doctorkafka/api/DoctorKafkaApi.java rename to doctork/src/main/java/com/pinterest/doctork/api/DoctorKApi.java index 64e56a8c..3fe87572 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/api/DoctorKafkaApi.java +++ b/doctork/src/main/java/com/pinterest/doctork/api/DoctorKApi.java @@ -1,28 +1,28 @@ -package com.pinterest.doctorkafka.api; +package com.pinterest.doctork.api; -import com.pinterest.doctorkafka.DoctorKafka; -import com.pinterest.doctorkafka.KafkaBroker; -import com.pinterest.doctorkafka.KafkaClusterManager; +import com.pinterest.doctork.DoctorK; +import com.pinterest.doctork.KafkaBroker; +import com.pinterest.doctork.KafkaClusterManager; import javax.ws.rs.NotFoundException; -public abstract class DoctorKafkaApi { - private DoctorKafka drkafka; +public abstract class DoctorKApi { + private DoctorK doctorK; - public DoctorKafkaApi() { - this.drkafka = null; + public DoctorKApi() { + this.doctorK = null; } - public DoctorKafkaApi(DoctorKafka drkafka) { - this.drkafka = drkafka; + public DoctorKApi(DoctorK doctorK) { + this.doctorK = doctorK; } - protected DoctorKafka getDrkafka() { - return drkafka; + protected DoctorK getDoctorK() { + return doctorK; } protected KafkaClusterManager checkAndGetClusterManager(String clusterName) { - KafkaClusterManager clusterManager = drkafka.getClusterManager(clusterName); + KafkaClusterManager clusterManager = doctorK.getClusterManager(clusterName); if (clusterManager == null) { throw new NotFoundException("Unknown clustername:" + clusterName); } diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/config/DoctorKafkaAppConfig.java b/doctork/src/main/java/com/pinterest/doctork/config/DoctorKAppConfig.java similarity index 75% rename from drkafka/src/main/java/com/pinterest/doctorkafka/config/DoctorKafkaAppConfig.java rename to doctork/src/main/java/com/pinterest/doctork/config/DoctorKAppConfig.java index 1dbb60d0..125c8960 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/config/DoctorKafkaAppConfig.java +++ b/doctork/src/main/java/com/pinterest/doctork/config/DoctorKAppConfig.java @@ -1,17 +1,17 @@ -package com.pinterest.doctorkafka.config; +package com.pinterest.doctork.config; import org.codehaus.jackson.annotate.JsonProperty; import org.hibernate.validator.constraints.NotEmpty; import io.dropwizard.Configuration; -public class DoctorKafkaAppConfig extends Configuration { +public class DoctorKAppConfig extends Configuration { @JsonProperty @NotEmpty private String config; - public DoctorKafkaAppConfig() { + public DoctorKAppConfig() { } /** diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/config/DoctorKafkaClusterConfig.java b/doctork/src/main/java/com/pinterest/doctork/config/DoctorKClusterConfig.java similarity index 95% rename from drkafka/src/main/java/com/pinterest/doctorkafka/config/DoctorKafkaClusterConfig.java rename to doctork/src/main/java/com/pinterest/doctork/config/DoctorKClusterConfig.java index f1a4aa94..568e44e6 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/config/DoctorKafkaClusterConfig.java +++ b/doctork/src/main/java/com/pinterest/doctork/config/DoctorKClusterConfig.java @@ -1,4 +1,4 @@ -package com.pinterest.doctorkafka.config; +package com.pinterest.doctork.config; import java.util.Map; @@ -17,7 +17,7 @@ * kafkacluster.data07.threshold.disk.usage=0.75 * */ -public class DoctorKafkaClusterConfig { +public class DoctorKClusterConfig { private static final String CONSUMER_PREFIX = "consumer."; private static final String DRYRUN = "dryrun"; @@ -42,7 +42,7 @@ public class DoctorKafkaClusterConfig { private String clusterName; private AbstractConfiguration clusterConfiguration; - public DoctorKafkaClusterConfig(String clusterName, AbstractConfiguration configuration) { + public DoctorKClusterConfig(String clusterName, AbstractConfiguration configuration) { this.clusterName = clusterName; this.clusterConfiguration = configuration; } @@ -120,7 +120,7 @@ public int getBrokerReplacementNoStatsSeconds() { public Map getConsumerConfigurations() { AbstractConfiguration sslConfiguration = new SubsetConfiguration(clusterConfiguration, CONSUMER_PREFIX); - return DoctorKafkaConfig.configurationToMap(sslConfiguration); + return DoctorKConfig.configurationToMap(sslConfiguration); } public SecurityProtocol getSecurityProtocol() { diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/config/DoctorKafkaConfig.java b/doctork/src/main/java/com/pinterest/doctork/config/DoctorKConfig.java similarity index 72% rename from drkafka/src/main/java/com/pinterest/doctorkafka/config/DoctorKafkaConfig.java rename to doctork/src/main/java/com/pinterest/doctork/config/DoctorKConfig.java index fd970479..dafef2f8 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/config/DoctorKafkaConfig.java +++ b/doctork/src/main/java/com/pinterest/doctork/config/DoctorKConfig.java @@ -1,5 +1,6 @@ -package com.pinterest.doctorkafka.config; +package com.pinterest.doctork.config; +import com.pinterest.doctork.security.DoctorKAuthorizationFilter; import org.apache.commons.configuration2.AbstractConfiguration; import org.apache.commons.configuration2.PropertiesConfiguration; import org.apache.commons.configuration2.SubsetConfiguration; @@ -8,8 +9,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import com.pinterest.doctorkafka.security.DrKafkaAuthorizationFilter; - import java.io.File; import java.util.Arrays; import java.util.HashMap; @@ -20,10 +19,10 @@ import java.util.Set; import java.util.stream.Collectors; -public class DoctorKafkaConfig { +public class DoctorKConfig { - private static final Logger LOG = LogManager.getLogger(DoctorKafkaConfig.class); - private static final String DOCTORKAFKA_PREFIX = "doctorkafka."; + private static final Logger LOG = LogManager.getLogger(DoctorKConfig.class); + private static final String DOCTORK_PREFIX = "doctork."; private static final String CLUSTER_PREFIX = "kafkacluster."; private static final String BROKERSTATS_CONSUMER_PREFIX = "brokerstats.consumer."; private static final String ACTION_REPORT_PRODUCER_PREFIX = "action.report.producer."; @@ -41,25 +40,25 @@ public class DoctorKafkaConfig { private static final String OSTRICH_PORT = "ostrich.port"; private static final String RESTART_DISABLE = "restart.disabled"; private static final String RESTART_INTERVAL_SECONDS = "restart.interval.seconds"; - private static final String DOCTORKAFKA_ZKURL = "zkurl"; + private static final String DOCTORK_ZKURL = "zkurl"; private static final String TSD_HOSTPORT = "tsd.hostport"; private static final String WEB_PORT = "web.port"; private static final String NOTIFICATION_EMAILS = "emails.notification"; private static final String ALERT_EMAILS = "emails.alert"; private static final String WEB_BIND_HOST = "web.bindhost"; - public static final String DRKAFKA_ADMIN_ROLE = "drkafka_admin"; - private static final String DRKAFKA_ADMIN_GROUPS = "admin.groups"; + public static final String DOCTORK_ADMIN_ROLE = "doctork_admin"; + private static final String DOCTORK_ADMIN_GROUPS = "admin.groups"; private static final String AUTHORIZATION_FILTER_CLASS = "authorization.filter.class"; private PropertiesConfiguration configuration = null; - private AbstractConfiguration drkafkaConfiguration = null; - private Map clusterConfigurations = null; + private AbstractConfiguration doctorKConfiguration = null; + private Map clusterConfigurations = null; - public DoctorKafkaConfig(String configPath) throws Exception { + public DoctorKConfig(String configPath) throws Exception { try { Configurations configurations = new Configurations(); configuration = configurations.properties(new File(configPath)); - drkafkaConfiguration = new SubsetConfiguration(configuration, DOCTORKAFKA_PREFIX); + doctorKConfiguration = new SubsetConfiguration(configuration, DOCTORK_PREFIX); this.initialize(); } catch (Exception e) { LOG.error("Failed to initialize configuration file {}", configPath, e); @@ -81,7 +80,7 @@ private void initialize() { SubsetConfiguration subsetConfiguration = new SubsetConfiguration(configuration, CLUSTER_PREFIX + cluster + "."); clusterConfigurations.put( - cluster, new DoctorKafkaClusterConfig(cluster, subsetConfiguration)); + cluster, new DoctorKClusterConfig(cluster, subsetConfiguration)); } } @@ -94,24 +93,24 @@ public Set getClusterZkUrls() { .collect(Collectors.toSet()); } - public String getDoctorKafkaZkurl() { - return drkafkaConfiguration.getString(DOCTORKAFKA_ZKURL); + public String getDoctorKZkurl() { + return doctorKConfiguration.getString(DOCTORK_ZKURL); } public String getBrokerstatsZkurl() { - return drkafkaConfiguration.getString(BROKERSTATS_ZKURL); + return doctorKConfiguration.getString(BROKERSTATS_ZKURL); } public String getBrokerStatsTopic() { - return drkafkaConfiguration.getString(BROKERSTATS_TOPIC); + return doctorKConfiguration.getString(BROKERSTATS_TOPIC); } public String getBrokerStatsVersion() { - return drkafkaConfiguration.getString(BROKERSTATS_VERSION); + return doctorKConfiguration.getString(BROKERSTATS_VERSION); } public long getBrokerStatsBacktrackWindowsInSeconds() { - String backtrackWindow = drkafkaConfiguration.getString(BROKERSTATS_BACKTRACK_SECONDS); + String backtrackWindow = doctorKConfiguration.getString(BROKERSTATS_BACKTRACK_SECONDS); return Long.parseLong(backtrackWindow); } @@ -120,7 +119,7 @@ public long getBrokerStatsBacktrackWindowsInSeconds() { * for writing to brokerstats kafka topic */ public Map getBrokerStatsConsumerSslConfigs() { - AbstractConfiguration sslConfiguration = new SubsetConfiguration(drkafkaConfiguration, BROKERSTATS_CONSUMER_PREFIX); + AbstractConfiguration sslConfiguration = new SubsetConfiguration(doctorKConfiguration, BROKERSTATS_CONSUMER_PREFIX); return configurationToMap(sslConfiguration); } @@ -131,16 +130,16 @@ public SecurityProtocol getBrokerStatsConsumerSecurityProtocol() { } public String getActionReportZkurl() { - return drkafkaConfiguration.getString(ACTION_REPORT_ZKURL); + return doctorKConfiguration.getString(ACTION_REPORT_ZKURL); } public String getActionReportTopic() { - return drkafkaConfiguration.getString(ACTION_REPORT_TOPIC); + return doctorKConfiguration.getString(ACTION_REPORT_TOPIC); } public Map getActionReportProducerSslConfigs() { AbstractConfiguration sslConfiguration = - new SubsetConfiguration(drkafkaConfiguration, ACTION_REPORT_PRODUCER_PREFIX); + new SubsetConfiguration(doctorKConfiguration, ACTION_REPORT_PRODUCER_PREFIX); return configurationToMap(sslConfiguration); } @@ -162,37 +161,37 @@ protected static Map configurationToMap(AbstractConfiguration c } public int getBrokerReplacementIntervalInSeconds() { - return drkafkaConfiguration.getInt(BROKER_REPLACEMENT_INTERVAL_SECONDS, 43200); + return doctorKConfiguration.getInt(BROKER_REPLACEMENT_INTERVAL_SECONDS, 43200); } public String getBrokerReplacementCommand() { - String command = drkafkaConfiguration.getString(BROKER_REPLACEMENT_COMMAND); + String command = doctorKConfiguration.getString(BROKER_REPLACEMENT_COMMAND); command = command.replaceAll("^\"|\"$", ""); return command; } public String getTsdHostPort() { - return drkafkaConfiguration.getString(TSD_HOSTPORT); + return doctorKConfiguration.getString(TSD_HOSTPORT); } public int getOstrichPort() { - return drkafkaConfiguration.getInt(OSTRICH_PORT, 0); + return doctorKConfiguration.getInt(OSTRICH_PORT, 0); } public long getRestartIntervalInSeconds() { - return drkafkaConfiguration.getLong(RESTART_INTERVAL_SECONDS); + return doctorKConfiguration.getLong(RESTART_INTERVAL_SECONDS); } public int getWebserverPort() { - return drkafkaConfiguration.getInteger(WEB_PORT, 8080); + return doctorKConfiguration.getInteger(WEB_PORT, 8080); } public String getWebserverBindHost() { - return drkafkaConfiguration.getString(WEB_BIND_HOST, "0.0.0.0"); + return doctorKConfiguration.getString(WEB_BIND_HOST, "0.0.0.0"); } - public DoctorKafkaClusterConfig getClusterConfigByZkUrl(String clusterZkUrl) { - for (DoctorKafkaClusterConfig clusterConfig : clusterConfigurations.values()) { + public DoctorKClusterConfig getClusterConfigByZkUrl(String clusterZkUrl) { + for (DoctorKClusterConfig clusterConfig : clusterConfigurations.values()) { if (clusterConfig.getZkUrl().equals(clusterZkUrl)) { return clusterConfig; } @@ -200,7 +199,7 @@ public DoctorKafkaClusterConfig getClusterConfigByZkUrl(String clusterZkUrl) { return null; } - public DoctorKafkaClusterConfig getClusterConfigByName(String clusterName) { + public DoctorKClusterConfig getClusterConfigByName(String clusterName) { return clusterConfigurations.get(clusterName); } @@ -209,7 +208,7 @@ public DoctorKafkaClusterConfig getClusterConfigByName(String clusterName) { * @return an array of email addresses for sending notification to. */ public String[] getNotificationEmails() { - String emailsStr = drkafkaConfiguration.getString(NOTIFICATION_EMAILS); + String emailsStr = doctorKConfiguration.getString(NOTIFICATION_EMAILS); return emailsStr.split(","); } @@ -218,12 +217,12 @@ public String[] getNotificationEmails() { * @return an array of email addresses for sending alerts to. */ public String[] getAlertEmails() { - String emailsStr = drkafkaConfiguration.getString(ALERT_EMAILS); + String emailsStr = doctorKConfiguration.getString(ALERT_EMAILS); return emailsStr.split(","); } public boolean getRestartDisabled(){ - return drkafkaConfiguration.getBoolean(RESTART_DISABLE, false); + return doctorKConfiguration.getBoolean(RESTART_DISABLE, false); } /** @@ -232,10 +231,10 @@ public boolean getRestartDisabled(){ * @throws ClassNotFoundException */ @SuppressWarnings("unchecked") - public Class getAuthorizationFilterClass() throws ClassNotFoundException { - if (drkafkaConfiguration.containsKey(AUTHORIZATION_FILTER_CLASS)) { - String classFqcn = drkafkaConfiguration.getString(AUTHORIZATION_FILTER_CLASS); - return (Class) Class.forName(classFqcn); + public Class getAuthorizationFilterClass() throws ClassNotFoundException { + if (doctorKConfiguration.containsKey(AUTHORIZATION_FILTER_CLASS)) { + String classFqcn = doctorKConfiguration.getString(AUTHORIZATION_FILTER_CLASS); + return (Class) Class.forName(classFqcn); } else { return null; } @@ -246,9 +245,9 @@ public Class getAuthorizationFilterClass() * permissions to run privileged commands. * @return list of groups */ - public List getDrKafkaAdminGroups() { - if (drkafkaConfiguration.containsKey(DRKAFKA_ADMIN_GROUPS)) { - return Arrays.asList(drkafkaConfiguration.getStringArray(DRKAFKA_ADMIN_GROUPS)); + public List getDoctorKAdminGroups() { + if (doctorKConfiguration.containsKey(DOCTORK_ADMIN_GROUPS)) { + return Arrays.asList(doctorKConfiguration.getStringArray(DOCTORK_ADMIN_GROUPS)); } else { return null; } diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/errors/ClusterInfoError.java b/doctork/src/main/java/com/pinterest/doctork/errors/ClusterInfoError.java similarity index 81% rename from drkafka/src/main/java/com/pinterest/doctorkafka/errors/ClusterInfoError.java rename to doctork/src/main/java/com/pinterest/doctork/errors/ClusterInfoError.java index e7e9fbeb..b6d7ff90 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/errors/ClusterInfoError.java +++ b/doctork/src/main/java/com/pinterest/doctork/errors/ClusterInfoError.java @@ -1,7 +1,5 @@ -package com.pinterest.doctorkafka.errors; +package com.pinterest.doctork.errors; -import java.util.ArrayList; -import java.util.List; import java.lang.Exception; public class ClusterInfoError extends Exception { diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/notification/Email.java b/doctork/src/main/java/com/pinterest/doctork/notification/Email.java similarity index 97% rename from drkafka/src/main/java/com/pinterest/doctorkafka/notification/Email.java rename to doctork/src/main/java/com/pinterest/doctork/notification/Email.java index 72e404ae..d8e4c26c 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/notification/Email.java +++ b/doctork/src/main/java/com/pinterest/doctork/notification/Email.java @@ -1,6 +1,6 @@ -package com.pinterest.doctorkafka.notification; +package com.pinterest.doctork.notification; -import com.pinterest.doctorkafka.KafkaBroker; +import com.pinterest.doctork.KafkaBroker; import kafka.cluster.Broker; import org.apache.commons.lang3.tuple.MutablePair; @@ -21,8 +21,8 @@ public class Email { private static final Logger LOG = LogManager.getLogger(Email.class); - private static final String TITLE_PREFIX = "doctorkafka : "; - private static final String TMP_FILE_PREFIX = "/tmp/doctorkafka_"; + private static final String TITLE_PREFIX = "doctork : "; + private static final String TMP_FILE_PREFIX = "/tmp/doctork_"; private static final long COOLOFF_INTERVAL = 1200000L; private static final Map reassignmentEmails = new ConcurrentHashMap<>(); diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/replicastats/BrokerStatsProcessor.java b/doctork/src/main/java/com/pinterest/doctork/replicastats/BrokerStatsProcessor.java similarity index 86% rename from drkafka/src/main/java/com/pinterest/doctorkafka/replicastats/BrokerStatsProcessor.java rename to doctork/src/main/java/com/pinterest/doctork/replicastats/BrokerStatsProcessor.java index ef828d93..ee779864 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/replicastats/BrokerStatsProcessor.java +++ b/doctork/src/main/java/com/pinterest/doctork/replicastats/BrokerStatsProcessor.java @@ -1,10 +1,9 @@ -package com.pinterest.doctorkafka.replicastats; +package com.pinterest.doctork.replicastats; -import com.pinterest.doctorkafka.BrokerStats; -import com.pinterest.doctorkafka.DoctorKafka; -import com.pinterest.doctorkafka.DoctorKafkaMetrics; -import com.pinterest.doctorkafka.util.OpenTsdbMetricConverter; -import com.pinterest.doctorkafka.util.OperatorUtil; +import com.pinterest.doctork.BrokerStats; +import com.pinterest.doctork.DoctorKMetrics; +import com.pinterest.doctork.util.OpenTsdbMetricConverter; +import com.pinterest.doctork.util.OperatorUtil; import java.util.Map; import org.apache.kafka.clients.consumer.ConsumerRecord; @@ -75,12 +74,12 @@ public void run() { BrokerStats brokerStats = OperatorUtil.deserializeBrokerStats(record); if (brokerStats == null || brokerStats.getName() == null) { // ignore the messages that the operator fails to deserialize - OpenTsdbMetricConverter.incr(DoctorKafkaMetrics.MESSAGE_DESERIALIZE_ERROR, 1); + OpenTsdbMetricConverter.incr(DoctorKMetrics.MESSAGE_DESERIALIZE_ERROR, 1); continue; } replicaStatsManager.update(brokerStats); - OpenTsdbMetricConverter.incr(DoctorKafkaMetrics.BROKERSTATS_MESSAGES, 1, + OpenTsdbMetricConverter.incr(DoctorKMetrics.BROKERSTATS_MESSAGES, 1, "zkUrl= " + brokerStats.getZkUrl()); } catch (Exception e) { LOG.debug("Fail to decode an message", e); diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/replicastats/PastReplicaStatsProcessor.java b/doctork/src/main/java/com/pinterest/doctork/replicastats/PastReplicaStatsProcessor.java similarity index 87% rename from drkafka/src/main/java/com/pinterest/doctorkafka/replicastats/PastReplicaStatsProcessor.java rename to doctork/src/main/java/com/pinterest/doctork/replicastats/PastReplicaStatsProcessor.java index 1ad8ff62..b65d2c3b 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/replicastats/PastReplicaStatsProcessor.java +++ b/doctork/src/main/java/com/pinterest/doctork/replicastats/PastReplicaStatsProcessor.java @@ -1,10 +1,10 @@ -package com.pinterest.doctorkafka.replicastats; +package com.pinterest.doctork.replicastats; -import com.pinterest.doctorkafka.BrokerStats; -import com.pinterest.doctorkafka.DoctorKafkaMetrics; -import com.pinterest.doctorkafka.util.KafkaUtils; -import com.pinterest.doctorkafka.util.OpenTsdbMetricConverter; -import com.pinterest.doctorkafka.util.OperatorUtil; +import com.pinterest.doctork.BrokerStats; +import com.pinterest.doctork.DoctorKMetrics; +import com.pinterest.doctork.util.KafkaUtils; +import com.pinterest.doctork.util.OpenTsdbMetricConverter; +import com.pinterest.doctork.util.OperatorUtil; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerRecord; @@ -61,7 +61,7 @@ public void run() { Properties props = new Properties(); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers); props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); - props.put(ConsumerConfig.GROUP_ID_CONFIG, "doctorkafka_" + topicPartition); + props.put(ConsumerConfig.GROUP_ID_CONFIG, "doctork_" + topicPartition); props.put(KafkaUtils.KEY_DESERIALIZER, "org.apache.kafka.common.serialization.ByteArrayDeserializer"); props.put(KafkaUtils.VALUE_DESERIALIZER, @@ -81,7 +81,7 @@ public void run() { for (ConsumerRecord record : records) { BrokerStats brokerStats = OperatorUtil.deserializeBrokerStats(record); if (brokerStats == null || brokerStats.getName() == null) { - OpenTsdbMetricConverter.incr(DoctorKafkaMetrics.MESSAGE_DESERIALIZE_ERROR, 1); + OpenTsdbMetricConverter.incr(DoctorKMetrics.MESSAGE_DESERIALIZE_ERROR, 1); continue; } replicaStatsManager.update(brokerStats); diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/replicastats/ReplicaStatsManager.java b/doctork/src/main/java/com/pinterest/doctork/replicastats/ReplicaStatsManager.java similarity index 88% rename from drkafka/src/main/java/com/pinterest/doctorkafka/replicastats/ReplicaStatsManager.java rename to doctork/src/main/java/com/pinterest/doctork/replicastats/ReplicaStatsManager.java index 27a30c41..641e16b0 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/replicastats/ReplicaStatsManager.java +++ b/doctork/src/main/java/com/pinterest/doctork/replicastats/ReplicaStatsManager.java @@ -1,4 +1,4 @@ -package com.pinterest.doctorkafka.replicastats; +package com.pinterest.doctork.replicastats; import java.util.ArrayList; import java.util.List; @@ -13,24 +13,24 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import com.pinterest.doctorkafka.BrokerStats; -import com.pinterest.doctorkafka.KafkaCluster; -import com.pinterest.doctorkafka.config.DoctorKafkaConfig; -import com.pinterest.doctorkafka.util.KafkaUtils; -import com.pinterest.doctorkafka.util.ReplicaStatsUtil; +import com.pinterest.doctork.BrokerStats; +import com.pinterest.doctork.KafkaCluster; +import com.pinterest.doctork.config.DoctorKConfig; +import com.pinterest.doctork.util.KafkaUtils; +import com.pinterest.doctork.util.ReplicaStatsUtil; public class ReplicaStatsManager { private static final Logger LOG = LogManager.getLogger(ReplicaStatsManager.class); private ConcurrentMap clusters = new ConcurrentHashMap<>(); - private DoctorKafkaConfig config; + private DoctorKConfig config; public ConcurrentMap getClusters() { return clusters; } - public DoctorKafkaConfig getConfig() { + public DoctorKConfig getConfig() { return config; } @@ -40,7 +40,7 @@ public Set getClusterZkUrls() { private Set clusterZkUrls; - public ReplicaStatsManager(DoctorKafkaConfig config){ + public ReplicaStatsManager(DoctorKConfig config){ this.config = config; this.clusterZkUrls = config.getClusterZkUrls(); } diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/security/DrKafkaAuthorizationFilter.java b/doctork/src/main/java/com/pinterest/doctork/security/DoctorKAuthorizationFilter.java similarity index 58% rename from drkafka/src/main/java/com/pinterest/doctorkafka/security/DrKafkaAuthorizationFilter.java rename to doctork/src/main/java/com/pinterest/doctork/security/DoctorKAuthorizationFilter.java index 6532e8e6..7a50cf77 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/security/DrKafkaAuthorizationFilter.java +++ b/doctork/src/main/java/com/pinterest/doctork/security/DoctorKAuthorizationFilter.java @@ -1,8 +1,8 @@ -package com.pinterest.doctorkafka.security; +package com.pinterest.doctork.security; import javax.ws.rs.container.ContainerRequestFilter; -import com.pinterest.doctorkafka.config.DoctorKafkaConfig; +import com.pinterest.doctork.config.DoctorKConfig; /** * This extends JAX-RS containter request filter for authorization. @@ -10,8 +10,8 @@ * Please refer to https://docs.oracle.com/javaee/7/api/javax/ws/rs/container/ContainerRequestFilter.html * for more details on how {@link ContainerRequestFilter} works */ -public interface DrKafkaAuthorizationFilter extends ContainerRequestFilter { +public interface DoctorKAuthorizationFilter extends ContainerRequestFilter { - public void configure(DoctorKafkaConfig config) throws Exception; + public void configure(DoctorKConfig config) throws Exception; } \ No newline at end of file diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/security/DrKafkaSecurityContext.java b/doctork/src/main/java/com/pinterest/doctork/security/DoctorKSecurityContext.java similarity index 65% rename from drkafka/src/main/java/com/pinterest/doctorkafka/security/DrKafkaSecurityContext.java rename to doctork/src/main/java/com/pinterest/doctork/security/DoctorKSecurityContext.java index 3a87d7b3..0d855dc5 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/security/DrKafkaSecurityContext.java +++ b/doctork/src/main/java/com/pinterest/doctork/security/DoctorKSecurityContext.java @@ -1,17 +1,17 @@ -package com.pinterest.doctorkafka.security; +package com.pinterest.doctork.security; import java.security.Principal; import java.util.Set; import javax.ws.rs.core.SecurityContext; -public class DrKafkaSecurityContext implements SecurityContext { +public class DoctorKSecurityContext implements SecurityContext { - private static final String DR_KAFKA_AUTH = "drkauth"; + private static final String DOCTORK_AUTH = "doctorkauth"; private UserPrincipal principal; private Set roles; - public DrKafkaSecurityContext(UserPrincipal principal, Set roles) { + public DoctorKSecurityContext(UserPrincipal principal, Set roles) { this.principal = principal; this.roles = roles; } @@ -33,12 +33,12 @@ public boolean isSecure() { @Override public String getAuthenticationScheme() { - return DR_KAFKA_AUTH; + return DOCTORK_AUTH; } @Override public String toString() { - return "DrKafkaSecurityContext [principal=" + principal + ", roles=" + roles + "]"; + return "DoctorKSecurityContext [principal=" + principal + ", roles=" + roles + "]"; } } \ No newline at end of file diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/security/SampleAuthorizationFilter.java b/doctork/src/main/java/com/pinterest/doctork/security/SampleAuthorizationFilter.java similarity index 73% rename from drkafka/src/main/java/com/pinterest/doctorkafka/security/SampleAuthorizationFilter.java rename to doctork/src/main/java/com/pinterest/doctork/security/SampleAuthorizationFilter.java index 372aa17e..d2c9af20 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/security/SampleAuthorizationFilter.java +++ b/doctork/src/main/java/com/pinterest/doctork/security/SampleAuthorizationFilter.java @@ -1,4 +1,4 @@ -package com.pinterest.doctorkafka.security; +package com.pinterest.doctork.security; import java.io.IOException; import java.util.Arrays; @@ -13,31 +13,31 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import com.pinterest.doctorkafka.config.DoctorKafkaConfig; +import com.pinterest.doctork.config.DoctorKConfig; import jersey.repackaged.com.google.common.collect.Sets; import jersey.repackaged.com.google.common.collect.Sets.SetView; /** - * This is a sample implementation of {@link DrKafkaAuthorizationFilter} + * This is a sample implementation of {@link DoctorKAuthorizationFilter} */ @Provider @Priority(1000) -public class SampleAuthorizationFilter implements DrKafkaAuthorizationFilter { +public class SampleAuthorizationFilter implements DoctorKAuthorizationFilter { private static final Logger LOG = LogManager.getLogger(SampleAuthorizationFilter.class); private static final String GROUPS_HEADER = "GROUPS"; private static final String USER_HEADER = "USER"; private Set allowedAdminGroups = new HashSet<>(); private static final Set ADMIN_ROLE_SET = new HashSet<>( - Arrays.asList(DoctorKafkaConfig.DRKAFKA_ADMIN_ROLE)); + Arrays.asList(DoctorKConfig.DOCTORK_ADMIN_ROLE)); private static final Set EMPTY_ROLE_SET = new HashSet<>(); @Override - public void configure(DoctorKafkaConfig config) throws Exception { - List drKafkaAdminGroups = config.getDrKafkaAdminGroups(); - if (drKafkaAdminGroups != null) { - allowedAdminGroups.addAll(drKafkaAdminGroups); + public void configure(DoctorKConfig config) throws Exception { + List doctorKAdminGroups = config.getDoctorKAdminGroups(); + if (doctorKAdminGroups != null) { + allowedAdminGroups.addAll(doctorKAdminGroups); LOG.info("Following groups will be allowed admin access:" + allowedAdminGroups); } } @@ -46,19 +46,19 @@ public void configure(DoctorKafkaConfig config) throws Exception { public void filter(ContainerRequestContext requestContext) throws IOException { String userHeader = requestContext.getHeaderString(USER_HEADER); String groupsHeader = requestContext.getHeaderString(GROUPS_HEADER); - DrKafkaSecurityContext ctx = null; + DoctorKSecurityContext ctx = null; if (userHeader != null && groupsHeader != null) { Set userGroups = new HashSet<>(Arrays.asList(groupsHeader.split(","))); SetView intersection = Sets.intersection(allowedAdminGroups, userGroups); if (intersection.size() > 0) { - ctx = new DrKafkaSecurityContext(new UserPrincipal(userHeader), ADMIN_ROLE_SET); + ctx = new DoctorKSecurityContext(new UserPrincipal(userHeader), ADMIN_ROLE_SET); requestContext.setSecurityContext(ctx); LOG.info("Received authenticated request, created context:" + ctx); return; } } - ctx = new DrKafkaSecurityContext(new UserPrincipal(userHeader), EMPTY_ROLE_SET); + ctx = new DoctorKSecurityContext(new UserPrincipal(userHeader), EMPTY_ROLE_SET); requestContext.setSecurityContext(ctx); LOG.info("Received annonymous request, bypassing authorizer"); } diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/security/UserPrincipal.java b/doctork/src/main/java/com/pinterest/doctork/security/UserPrincipal.java similarity index 89% rename from drkafka/src/main/java/com/pinterest/doctorkafka/security/UserPrincipal.java rename to doctork/src/main/java/com/pinterest/doctork/security/UserPrincipal.java index 873876f2..4a200f9d 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/security/UserPrincipal.java +++ b/doctork/src/main/java/com/pinterest/doctork/security/UserPrincipal.java @@ -1,4 +1,4 @@ -package com.pinterest.doctorkafka.security; +package com.pinterest.doctork.security; import java.security.Principal; diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/servlet/ClusterInfoServlet.java b/doctork/src/main/java/com/pinterest/doctork/servlet/ClusterInfoServlet.java similarity index 91% rename from drkafka/src/main/java/com/pinterest/doctorkafka/servlet/ClusterInfoServlet.java rename to doctork/src/main/java/com/pinterest/doctork/servlet/ClusterInfoServlet.java index 3bf685c6..420c4278 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/servlet/ClusterInfoServlet.java +++ b/doctork/src/main/java/com/pinterest/doctork/servlet/ClusterInfoServlet.java @@ -1,11 +1,11 @@ -package com.pinterest.doctorkafka.servlet; +package com.pinterest.doctork.servlet; import com.google.gson.Gson; -import com.pinterest.doctorkafka.KafkaBroker; -import com.pinterest.doctorkafka.DoctorKafkaMain; -import com.pinterest.doctorkafka.KafkaCluster; -import com.pinterest.doctorkafka.KafkaClusterManager; -import com.pinterest.doctorkafka.errors.ClusterInfoError; +import com.pinterest.doctork.KafkaBroker; +import com.pinterest.doctork.DoctorKMain; +import com.pinterest.doctork.KafkaCluster; +import com.pinterest.doctork.KafkaClusterManager; +import com.pinterest.doctork.errors.ClusterInfoError; import kafka.cluster.Broker; import org.apache.logging.log4j.LogManager; @@ -18,7 +18,7 @@ import java.util.TreeMap; import java.util.TreeSet; -public class ClusterInfoServlet extends DoctorKafkaServlet { +public class ClusterInfoServlet extends DoctorKServlet { private static final Logger LOG = LogManager.getLogger(ClusterInfoServlet.class); private static final Gson gson = new Gson(); @@ -34,7 +34,7 @@ public void renderJSON(PrintWriter writer, Map params) { } try { - KafkaClusterManager clusterManager = DoctorKafkaMain.doctorKafka.getClusterManager(clusterName); + KafkaClusterManager clusterManager = DoctorKMain.doctorK.getClusterManager(clusterName); if (clusterManager == null) { ClusterInfoError error = new ClusterInfoError("Failed to find cluster manager for " + clusterName); writer.print(gson.toJson(error)); @@ -53,7 +53,7 @@ public void renderHTML(PrintWriter writer, Map params) { printHeader(writer); String clusterName = params.get("name"); KafkaClusterManager clusterMananger; - clusterMananger = DoctorKafkaMain.doctorKafka.getClusterManager(clusterName); + clusterMananger = DoctorKMain.doctorK.getClusterManager(clusterName); if (clusterMananger == null) { writer.print("Failed to find cluster manager for " + clusterName); return; diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/servlet/DoctorKafkaActionsServlet.java b/doctork/src/main/java/com/pinterest/doctork/servlet/DoctorKActionsServlet.java similarity index 85% rename from drkafka/src/main/java/com/pinterest/doctorkafka/servlet/DoctorKafkaActionsServlet.java rename to doctork/src/main/java/com/pinterest/doctork/servlet/DoctorKActionsServlet.java index 77a20226..7fc192f3 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/servlet/DoctorKafkaActionsServlet.java +++ b/doctork/src/main/java/com/pinterest/doctork/servlet/DoctorKActionsServlet.java @@ -1,9 +1,9 @@ -package com.pinterest.doctorkafka.servlet; +package com.pinterest.doctork.servlet; -import com.pinterest.doctorkafka.DoctorKafkaMain; -import com.pinterest.doctorkafka.OperatorAction; -import com.pinterest.doctorkafka.config.DoctorKafkaConfig; -import com.pinterest.doctorkafka.util.OperatorUtil; +import com.pinterest.doctork.DoctorKMain; +import com.pinterest.doctork.OperatorAction; +import com.pinterest.doctork.config.DoctorKConfig; +import com.pinterest.doctork.util.OperatorUtil; import com.google.common.collect.Lists; import org.apache.avro.Schema; @@ -29,11 +29,11 @@ import java.util.Properties; -public class DoctorKafkaActionsServlet extends DoctorKafkaServlet { +public class DoctorKActionsServlet extends DoctorKServlet { - private static final Logger LOG = LogManager.getLogger(DoctorKafkaActionsServlet.class); + private static final Logger LOG = LogManager.getLogger(DoctorKActionsServlet.class); private static final Gson gson = new Gson(); - private static final String OPERATOR_ACTIONS_CONSUMER_GROUP = "doctorkafka_actions_consumer"; + private static final String OPERATOR_ACTIONS_CONSUMER_GROUP = "doctork_actions_consumer"; private static final int NUM_MESSAGES = 1000; private static final long CONSUMER_POLL_TIMEOUT_MS = 1000L; private static final DecoderFactory avroDecoderFactory = DecoderFactory.get(); @@ -68,7 +68,7 @@ public void renderJSON(PrintWriter writer, Map params) { @Override public void renderHTML(PrintWriter writer, Map params) { printHeader(writer); - writer.print("

Home > doctorkafka action

"); + writer.print("

Home > doctork action

"); writer.print(" "); writer.print(" "); writer.print(" "); @@ -104,13 +104,13 @@ public void renderHTML(PrintWriter writer, Map params) { private List> retrieveActionReportMessages() { - DoctorKafkaConfig doctorKafkaConfig = DoctorKafkaMain.doctorKafka.getDoctorKafkaConfig(); - String zkUrl = doctorKafkaConfig.getBrokerstatsZkurl(); - String actionReportTopic = doctorKafkaConfig.getActionReportTopic(); + DoctorKConfig doctorKConfig = DoctorKMain.doctorK.getDoctorKConfig(); + String zkUrl = doctorKConfig.getBrokerstatsZkurl(); + String actionReportTopic = doctorKConfig.getActionReportTopic(); Properties properties = OperatorUtil.createKafkaConsumerProperties(zkUrl, OPERATOR_ACTIONS_CONSUMER_GROUP, - doctorKafkaConfig.getActionReportProducerSecurityProtocol(), - doctorKafkaConfig.getActionReportProducerSslConfigs()); + doctorKConfig.getActionReportProducerSecurityProtocol(), + doctorKConfig.getActionReportProducerSslConfigs()); KafkaConsumer consumer = new KafkaConsumer<>(properties); TopicPartition operatorReportTopicPartition = new TopicPartition(actionReportTopic, 0); diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/servlet/DoctorKafkaBrokerStatsServlet.java b/doctork/src/main/java/com/pinterest/doctork/servlet/DoctorKBrokerStatsServlet.java similarity index 88% rename from drkafka/src/main/java/com/pinterest/doctorkafka/servlet/DoctorKafkaBrokerStatsServlet.java rename to doctork/src/main/java/com/pinterest/doctork/servlet/DoctorKBrokerStatsServlet.java index 4748b219..25dbf4da 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/servlet/DoctorKafkaBrokerStatsServlet.java +++ b/doctork/src/main/java/com/pinterest/doctork/servlet/DoctorKBrokerStatsServlet.java @@ -1,13 +1,13 @@ -package com.pinterest.doctorkafka.servlet; +package com.pinterest.doctork.servlet; -import com.pinterest.doctorkafka.BrokerStats; -import com.pinterest.doctorkafka.DoctorKafkaMain; -import com.pinterest.doctorkafka.KafkaBroker; -import com.pinterest.doctorkafka.KafkaCluster; -import com.pinterest.doctorkafka.KafkaClusterManager; -import com.pinterest.doctorkafka.ReplicaStat; -import com.pinterest.doctorkafka.util.KafkaUtils; -import com.pinterest.doctorkafka.errors.ClusterInfoError; +import com.pinterest.doctork.BrokerStats; +import com.pinterest.doctork.DoctorKMain; +import com.pinterest.doctork.KafkaBroker; +import com.pinterest.doctork.KafkaCluster; +import com.pinterest.doctork.KafkaClusterManager; +import com.pinterest.doctork.ReplicaStat; +import com.pinterest.doctork.util.KafkaUtils; +import com.pinterest.doctork.errors.ClusterInfoError; import org.apache.kafka.common.TopicPartition; import org.apache.logging.log4j.LogManager; @@ -23,14 +23,14 @@ import java.util.Map; import java.util.TreeMap; -public class DoctorKafkaBrokerStatsServlet extends DoctorKafkaServlet { +public class DoctorKBrokerStatsServlet extends DoctorKServlet { - private static final Logger LOG = LogManager.getLogger(DoctorKafkaBrokerStatsServlet.class); + private static final Logger LOG = LogManager.getLogger(DoctorKBrokerStatsServlet.class); private static final Gson gson = (new GsonBuilder()).serializeSpecialFloatingPointValues().create(); public KafkaBroker getBroker(String clusterName, int brokerId) throws ClusterInfoError { KafkaClusterManager clusterMananger = - DoctorKafkaMain.doctorKafka.getClusterManager(clusterName); + DoctorKMain.doctorK.getClusterManager(clusterName); if (clusterMananger == null) { throw new ClusterInfoError("Failed to find cluster manager for {}", clusterName); } @@ -64,7 +64,7 @@ public void renderJSON(PrintWriter writer, Map params) { int brokerId = Integer.parseInt(params.get("brokerid")); String clusterName = params.get("cluster"); KafkaClusterManager clusterMananger = - DoctorKafkaMain.doctorKafka.getClusterManager(clusterName); + DoctorKMain.doctorK.getClusterManager(clusterName); KafkaCluster cluster = clusterMananger.getCluster(); KafkaBroker broker = cluster.brokers.get(brokerId); writer.print(broker.toJson()); diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/servlet/DoctorKafkaInfoServlet.java b/doctork/src/main/java/com/pinterest/doctork/servlet/DoctorKInfoServlet.java similarity index 87% rename from drkafka/src/main/java/com/pinterest/doctorkafka/servlet/DoctorKafkaInfoServlet.java rename to doctork/src/main/java/com/pinterest/doctork/servlet/DoctorKInfoServlet.java index 8d2312dd..4f11643c 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/servlet/DoctorKafkaInfoServlet.java +++ b/doctork/src/main/java/com/pinterest/doctork/servlet/DoctorKInfoServlet.java @@ -1,8 +1,8 @@ -package com.pinterest.doctorkafka.servlet; +package com.pinterest.doctork.servlet; -import com.pinterest.doctorkafka.KafkaClusterManager; -import com.pinterest.doctorkafka.DoctorKafkaMain; +import com.pinterest.doctork.KafkaClusterManager; +import com.pinterest.doctork.DoctorKMain; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -16,9 +16,9 @@ import java.util.Map; import java.util.TreeMap; -public class DoctorKafkaInfoServlet extends DoctorKafkaServlet { +public class DoctorKInfoServlet extends DoctorKServlet { - private static final Logger LOG = LogManager.getLogger(DoctorKafkaInfoServlet.class); + private static final Logger LOG = LogManager.getLogger(DoctorKInfoServlet.class); private static final Gson gson = new Gson(); @Override @@ -29,7 +29,7 @@ public void renderJSON(PrintWriter writer, Map params) { JsonArray jsonClusters = new JsonArray(); json.add("clusters", jsonClusters); - Collection clusterManagers = DoctorKafkaMain.doctorKafka.getClusterManagers(); + Collection clusterManagers = DoctorKMain.doctorK.getClusterManagers(); for (KafkaClusterManager clusterManager : clusterManagers) { JsonObject cluster = new JsonObject(); @@ -58,7 +58,7 @@ public void renderHTML(PrintWriter writer, Map params) { writer.print("

Version: " + version + ", Uptime: " + jvmUpTimeInSeconds + " seconds

"); writer.print(""); - Collection clusterManagers = DoctorKafkaMain.doctorKafka.getClusterManagers(); + Collection clusterManagers = DoctorKMain.doctorK.getClusterManagers(); writer.print("
"); writer.print("
Timestamp Cluster
"); writer.print(""); diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/servlet/DoctorKafkaServlet.java b/doctork/src/main/java/com/pinterest/doctork/servlet/DoctorKServlet.java similarity index 94% rename from drkafka/src/main/java/com/pinterest/doctorkafka/servlet/DoctorKafkaServlet.java rename to doctork/src/main/java/com/pinterest/doctork/servlet/DoctorKServlet.java index 5f26ad88..d6a606d5 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/servlet/DoctorKafkaServlet.java +++ b/doctork/src/main/java/com/pinterest/doctork/servlet/DoctorKServlet.java @@ -1,8 +1,8 @@ -package com.pinterest.doctorkafka.servlet; +package com.pinterest.doctork.servlet; import static java.util.stream.Collectors.toList; -import com.pinterest.doctorkafka.DoctorKafkaMain; +import com.pinterest.doctork.DoctorKMain; import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; import org.apache.kafka.common.TopicPartition; @@ -25,14 +25,14 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -public class DoctorKafkaServlet extends HttpServlet { - private static final Logger LOG = LogManager.getLogger(DoctorKafkaWebServer.class); +public class DoctorKServlet extends HttpServlet { + private static final Logger LOG = LogManager.getLogger(DoctorKWebServer.class); public static String getVersion() { String versionString = ""; try { final Properties properties = new Properties(); - InputStream inputStream = DoctorKafkaMain.class.getResourceAsStream("/versioning.properties"); + InputStream inputStream = DoctorKMain.class.getResourceAsStream("/versioning.properties"); properties.load(inputStream); versionString = properties.getProperty("version"); } catch (IOException e) { @@ -74,7 +74,7 @@ public static void printHeader(PrintWriter writer) { writer.print("

"); writer.print(""); - writer.print("DoctorKafka

"); + writer.print("DoctorK"); writer.print(""); } diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/servlet/DoctorKafkaWebServer.java b/doctork/src/main/java/com/pinterest/doctork/servlet/DoctorKWebServer.java similarity index 88% rename from drkafka/src/main/java/com/pinterest/doctorkafka/servlet/DoctorKafkaWebServer.java rename to doctork/src/main/java/com/pinterest/doctork/servlet/DoctorKWebServer.java index ba88004e..92a0c32a 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/servlet/DoctorKafkaWebServer.java +++ b/doctork/src/main/java/com/pinterest/doctork/servlet/DoctorKWebServer.java @@ -1,4 +1,4 @@ -package com.pinterest.doctorkafka.servlet; +package com.pinterest.doctork.servlet; import com.google.common.base.Charsets; import com.google.common.io.CharStreams; @@ -17,15 +17,15 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -public class DoctorKafkaWebServer implements Runnable { +public class DoctorKWebServer implements Runnable { - private static final Logger LOG = LogManager.getLogger(DoctorKafkaWebServer.class); + private static final Logger LOG = LogManager.getLogger(DoctorKWebServer.class); private static final String WEBAPP_DIR = "webapp/pages/index.html"; private Thread thread; private int port; - public DoctorKafkaWebServer(int port) { + public DoctorKWebServer(int port) { this.port = port; } @@ -39,7 +39,7 @@ public void run() { try { Server server = new Server(port); - URL warUrl = DoctorKafkaWebServer.class.getClassLoader().getResource(WEBAPP_DIR); + URL warUrl = DoctorKWebServer.class.getClassLoader().getResource(WEBAPP_DIR); if (warUrl == null) { LOG.error("warUrl is null"); } @@ -54,7 +54,7 @@ public void run() { } server.join(); } catch (Exception e) { - LOG.error("Exception in DoctorKafkaWebServer.", e); + LOG.error("Exception in DoctorKWebServer.", e); } } diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/servlet/KafkaTopicStatsServlet.java b/doctork/src/main/java/com/pinterest/doctork/servlet/KafkaTopicStatsServlet.java similarity index 89% rename from drkafka/src/main/java/com/pinterest/doctorkafka/servlet/KafkaTopicStatsServlet.java rename to doctork/src/main/java/com/pinterest/doctork/servlet/KafkaTopicStatsServlet.java index b66b158a..8451168c 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/servlet/KafkaTopicStatsServlet.java +++ b/doctork/src/main/java/com/pinterest/doctork/servlet/KafkaTopicStatsServlet.java @@ -1,11 +1,11 @@ -package com.pinterest.doctorkafka.servlet; +package com.pinterest.doctork.servlet; -import com.pinterest.doctorkafka.DoctorKafkaMain; -import com.pinterest.doctorkafka.KafkaCluster; -import com.pinterest.doctorkafka.KafkaClusterManager; -import com.pinterest.doctorkafka.util.KafkaUtils; -import com.pinterest.doctorkafka.errors.ClusterInfoError; +import com.pinterest.doctork.DoctorKMain; +import com.pinterest.doctork.KafkaCluster; +import com.pinterest.doctork.KafkaClusterManager; +import com.pinterest.doctork.util.KafkaUtils; +import com.pinterest.doctork.errors.ClusterInfoError; import org.apache.kafka.common.TopicPartition; import org.apache.logging.log4j.LogManager; @@ -18,7 +18,7 @@ import java.util.Map; import java.util.TreeSet; -public class KafkaTopicStatsServlet extends DoctorKafkaServlet { +public class KafkaTopicStatsServlet extends DoctorKServlet { private static final Logger LOG = LogManager.getLogger(KafkaTopicStatsServlet.class); private static final Gson gson = new Gson(); @@ -30,7 +30,7 @@ public void renderJSON(PrintWriter writer, Map params) { JsonArray json = new JsonArray(); KafkaClusterManager clusterMananger = - DoctorKafkaMain.doctorKafka.getClusterManager(clusterName); + DoctorKMain.doctorK.getClusterManager(clusterName); if (clusterMananger == null) { ClusterInfoError error = new ClusterInfoError("Failed to find cluster manager for {}", clusterName); writer.print(gson.toJson(error)); @@ -74,7 +74,7 @@ public void renderHTML(PrintWriter writer, Map params) { // generate the HTML markups KafkaClusterManager clusterMananger = - DoctorKafkaMain.doctorKafka.getClusterManager(clusterName); + DoctorKMain.doctorK.getClusterManager(clusterName); if (clusterMananger == null) { writer.print("Failed to find cluster manager for " + clusterName); return; diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/servlet/UnderReplicatedPartitionsServlet.java b/doctork/src/main/java/com/pinterest/doctork/servlet/UnderReplicatedPartitionsServlet.java similarity index 81% rename from drkafka/src/main/java/com/pinterest/doctorkafka/servlet/UnderReplicatedPartitionsServlet.java rename to doctork/src/main/java/com/pinterest/doctork/servlet/UnderReplicatedPartitionsServlet.java index 45af7f59..bbb30913 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/servlet/UnderReplicatedPartitionsServlet.java +++ b/doctork/src/main/java/com/pinterest/doctork/servlet/UnderReplicatedPartitionsServlet.java @@ -1,9 +1,9 @@ -package com.pinterest.doctorkafka.servlet; +package com.pinterest.doctork.servlet; -import com.pinterest.doctorkafka.DoctorKafkaMain; -import com.pinterest.doctorkafka.KafkaClusterManager; -import com.pinterest.doctorkafka.errors.ClusterInfoError; +import com.pinterest.doctork.DoctorKMain; +import com.pinterest.doctork.KafkaClusterManager; +import com.pinterest.doctork.errors.ClusterInfoError; import org.apache.kafka.common.PartitionInfo; import org.apache.logging.log4j.LogManager; @@ -15,7 +15,7 @@ import java.util.List; import java.util.Map; -public class UnderReplicatedPartitionsServlet extends DoctorKafkaServlet { +public class UnderReplicatedPartitionsServlet extends DoctorKServlet { private static final Logger LOG = LogManager.getLogger(UnderReplicatedPartitionsServlet.class); private static final Gson gson = new Gson(); @@ -24,7 +24,7 @@ public class UnderReplicatedPartitionsServlet extends DoctorKafkaServlet { public void renderJSON(PrintWriter writer, Map params) { String clusterName = params.get("cluster"); KafkaClusterManager clusterMananger = - DoctorKafkaMain.doctorKafka.getClusterManager(clusterName); + DoctorKMain.doctorK.getClusterManager(clusterName); if (clusterMananger == null) { ClusterInfoError error = new ClusterInfoError("Failed to find cluster manager for {}", clusterName); @@ -47,7 +47,7 @@ public void renderHTML(PrintWriter writer, Map params) { printHeader(writer); KafkaClusterManager clusterMananger = - DoctorKafkaMain.doctorKafka.getClusterManager(clusterName); + DoctorKMain.doctorK.getClusterManager(clusterName); if (clusterMananger == null) { writer.print("Failed to find cluster manager for " + clusterName); diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/tools/BrokerReplacement.java b/doctork/src/main/java/com/pinterest/doctork/tools/BrokerReplacement.java similarity index 95% rename from drkafka/src/main/java/com/pinterest/doctorkafka/tools/BrokerReplacement.java rename to doctork/src/main/java/com/pinterest/doctork/tools/BrokerReplacement.java index 8f2bbc0a..0b190caf 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/tools/BrokerReplacement.java +++ b/doctork/src/main/java/com/pinterest/doctork/tools/BrokerReplacement.java @@ -1,6 +1,6 @@ -package com.pinterest.doctorkafka.tools; +package com.pinterest.doctork.tools; -import com.pinterest.doctorkafka.util.BrokerReplacer; +import com.pinterest.doctork.util.BrokerReplacer; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/tools/BrokerStatsFilter.java b/doctork/src/main/java/com/pinterest/doctork/tools/BrokerStatsFilter.java similarity index 96% rename from drkafka/src/main/java/com/pinterest/doctorkafka/tools/BrokerStatsFilter.java rename to doctork/src/main/java/com/pinterest/doctork/tools/BrokerStatsFilter.java index 00a4536b..0d6facd0 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/tools/BrokerStatsFilter.java +++ b/doctork/src/main/java/com/pinterest/doctork/tools/BrokerStatsFilter.java @@ -1,9 +1,9 @@ -package com.pinterest.doctorkafka.tools; +package com.pinterest.doctork.tools; -import com.pinterest.doctorkafka.BrokerStats; -import com.pinterest.doctorkafka.util.KafkaUtils; -import com.pinterest.doctorkafka.util.OperatorUtil; -import com.pinterest.doctorkafka.util.ReplicaStatsUtil; +import com.pinterest.doctork.BrokerStats; +import com.pinterest.doctork.util.KafkaUtils; +import com.pinterest.doctork.util.OperatorUtil; +import com.pinterest.doctork.util.ReplicaStatsUtil; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/tools/ClusterLoadBalancer.java b/doctork/src/main/java/com/pinterest/doctork/tools/ClusterLoadBalancer.java similarity index 88% rename from drkafka/src/main/java/com/pinterest/doctorkafka/tools/ClusterLoadBalancer.java rename to doctork/src/main/java/com/pinterest/doctork/tools/ClusterLoadBalancer.java index e34511ed..67a1e9a0 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/tools/ClusterLoadBalancer.java +++ b/doctork/src/main/java/com/pinterest/doctork/tools/ClusterLoadBalancer.java @@ -1,11 +1,11 @@ -package com.pinterest.doctorkafka.tools; +package com.pinterest.doctork.tools; -import com.pinterest.doctorkafka.KafkaBroker; -import com.pinterest.doctorkafka.KafkaCluster; -import com.pinterest.doctorkafka.KafkaClusterManager; -import com.pinterest.doctorkafka.config.DoctorKafkaClusterConfig; -import com.pinterest.doctorkafka.config.DoctorKafkaConfig; -import com.pinterest.doctorkafka.replicastats.ReplicaStatsManager; +import com.pinterest.doctork.KafkaBroker; +import com.pinterest.doctork.KafkaCluster; +import com.pinterest.doctork.KafkaClusterManager; +import com.pinterest.doctork.config.DoctorKClusterConfig; +import com.pinterest.doctork.config.DoctorKConfig; +import com.pinterest.doctork.replicastats.ReplicaStatsManager; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; @@ -47,7 +47,7 @@ public class ClusterLoadBalancer { private static CommandLine parseCommandLine(String[] args) { Option brokerStatsZookeeper = new Option(BROKERSTATS_ZOOKEEPER, true, "zookeeper for brokerstats topic"); - Option config = new Option(CONFIG, true, "doctorkafka config"); + Option config = new Option(CONFIG, true, "doctork config"); Option brokerStatsTopic = new Option(BROKERSTATS_TOPIC, true, "topic for brokerstats"); Option clusterZookeeper = new Option(CLUSTER_ZOOKEEPER, true, "cluster zookeeper"); Option seconds = new Option(SECONDS, true, "examined time window in seconds"); @@ -85,7 +85,7 @@ public static void main(String[] args) throws Exception { long seconds = Long.parseLong(commandLine.getOptionValue(SECONDS)); boolean onlyOne = commandLine.hasOption(ONLY_ONE); - ReplicaStatsManager replicaStatsManager = new ReplicaStatsManager(new DoctorKafkaConfig(configFilePath)); + ReplicaStatsManager replicaStatsManager = new ReplicaStatsManager(new DoctorKConfig(configFilePath)); replicaStatsManager.readPastReplicaStats(brokerStatsZk, SecurityProtocol.PLAINTEXT, brokerStatsTopic, seconds); Set zkUrls = replicaStatsManager.getClusterZkUrls(); @@ -94,7 +94,7 @@ public static void main(String[] args) throws Exception { return; } - DoctorKafkaClusterConfig clusterConf = + DoctorKClusterConfig clusterConf = replicaStatsManager.getConfig().getClusterConfigByZkUrl(clusterZk); KafkaCluster kafkaCluster = replicaStatsManager.getClusters().get(clusterZk); KafkaClusterManager clusterManager = new KafkaClusterManager( diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/tools/DoctorKafkaActionRetriever.java b/doctork/src/main/java/com/pinterest/doctork/tools/DoctorKActionRetriever.java similarity index 92% rename from drkafka/src/main/java/com/pinterest/doctorkafka/tools/DoctorKafkaActionRetriever.java rename to doctork/src/main/java/com/pinterest/doctork/tools/DoctorKActionRetriever.java index c448d028..2b7b0ea9 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/tools/DoctorKafkaActionRetriever.java +++ b/doctork/src/main/java/com/pinterest/doctork/tools/DoctorKActionRetriever.java @@ -1,7 +1,7 @@ -package com.pinterest.doctorkafka.tools; +package com.pinterest.doctork.tools; -import com.pinterest.doctorkafka.OperatorAction; -import com.pinterest.doctorkafka.util.OperatorUtil; +import com.pinterest.doctork.OperatorAction; +import com.pinterest.doctork.util.OperatorUtil; import com.google.common.collect.Lists; import org.apache.avro.Schema; @@ -29,9 +29,9 @@ import java.util.Map; import java.util.Properties; -public class DoctorKafkaActionRetriever { +public class DoctorKActionRetriever { - private static final Logger LOG = LogManager.getLogger(DoctorKafkaActionRetriever.class); + private static final Logger LOG = LogManager.getLogger(DoctorKActionRetriever.class); private static final DecoderFactory avroDecoderFactory = DecoderFactory.get(); private static Schema operatorActionSchema = OperatorAction.getClassSchema(); @@ -46,8 +46,8 @@ public class DoctorKafkaActionRetriever { * -topic operator_report -num_messages 1000 */ private static CommandLine parseCommandLine(String[] args) { - Option zookeeper = new Option(ZOOKEEPER, true, "doctorkafka action zookeeper"); - Option topic = new Option(TOPIC, true, "doctorkafka action topic"); + Option zookeeper = new Option(ZOOKEEPER, true, "doctork action zookeeper"); + Option topic = new Option(TOPIC, true, "doctork action topic"); Option num_messages = new Option(NUM_MESSAGES, true, "num of messages to retrieve"); options.addOption(zookeeper).addOption(topic).addOption(num_messages); diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/tools/DoctorKafkaActionWriter.java b/doctork/src/main/java/com/pinterest/doctork/tools/DoctorKActionWriter.java similarity index 87% rename from drkafka/src/main/java/com/pinterest/doctorkafka/tools/DoctorKafkaActionWriter.java rename to doctork/src/main/java/com/pinterest/doctork/tools/DoctorKActionWriter.java index 4bf69c95..76e59f5a 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/tools/DoctorKafkaActionWriter.java +++ b/doctork/src/main/java/com/pinterest/doctork/tools/DoctorKActionWriter.java @@ -1,6 +1,6 @@ -package com.pinterest.doctorkafka.tools; +package com.pinterest.doctork.tools; -import com.pinterest.doctorkafka.DoctorKafkaActionReporter; +import com.pinterest.doctork.DoctorKActionReporter; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; @@ -11,7 +11,7 @@ import org.apache.commons.cli.ParseException; import org.apache.kafka.common.security.auth.SecurityProtocol; -public class DoctorKafkaActionWriter { +public class DoctorKActionWriter { private static final String ZOOKEEPER = "zookeeper"; private static final String TOPIC = "topic"; @@ -56,8 +56,8 @@ public static void main(String[] args) throws Exception { String topic = commandLine.getOptionValue(TOPIC); String message = commandLine.getOptionValue(MESSAGE); - DoctorKafkaActionReporter actionReporter = - new DoctorKafkaActionReporter(zkUrl, SecurityProtocol.PLAINTEXT, topic, null); + DoctorKActionReporter actionReporter = + new DoctorKActionReporter(zkUrl, SecurityProtocol.PLAINTEXT, topic, null); actionReporter.sendMessage("testkafka10", message); } } diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/tools/DoctorKafkaZookeeperClient.java b/doctork/src/main/java/com/pinterest/doctork/tools/DoctorKZookeeperClient.java similarity index 90% rename from drkafka/src/main/java/com/pinterest/doctorkafka/tools/DoctorKafkaZookeeperClient.java rename to doctork/src/main/java/com/pinterest/doctork/tools/DoctorKZookeeperClient.java index 4d3182b4..6ad5b5e4 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/tools/DoctorKafkaZookeeperClient.java +++ b/doctork/src/main/java/com/pinterest/doctork/tools/DoctorKZookeeperClient.java @@ -1,6 +1,6 @@ -package com.pinterest.doctorkafka.tools; +package com.pinterest.doctork.tools; -import com.pinterest.doctorkafka.util.ZookeeperClient; +import com.pinterest.doctork.util.ZookeeperClient; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; @@ -12,7 +12,7 @@ import java.util.Date; -public class DoctorKafkaZookeeperClient { +public class DoctorKZookeeperClient { private static final String ZOOKEEPER = "zookeeper"; private static final String CLUSTER = "cluster"; @@ -20,7 +20,7 @@ public class DoctorKafkaZookeeperClient { private static final Options options = new Options(); /** - * Usage: DoctorKafkaZookeeperClient \ + * Usage: DoctorKZookeeperClient \ * --zookeeper zookeeper001:2181/cluster1 --cluster cluster1 \ * --command terminate --broker broker1 */ @@ -46,7 +46,7 @@ private static CommandLine parseCommandLine(String[] args) { private static void printUsageAndExit() { HelpFormatter formatter = new HelpFormatter(); - formatter.printHelp("DoctorKafkaZookeeperClient", options); + formatter.printHelp("DoctorKZookeeperClient", options); System.exit(1); } diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/tools/KafkaWriter.java b/doctork/src/main/java/com/pinterest/doctork/tools/KafkaWriter.java similarity index 97% rename from drkafka/src/main/java/com/pinterest/doctorkafka/tools/KafkaWriter.java rename to doctork/src/main/java/com/pinterest/doctork/tools/KafkaWriter.java index de7a4333..848b3892 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/tools/KafkaWriter.java +++ b/doctork/src/main/java/com/pinterest/doctork/tools/KafkaWriter.java @@ -1,6 +1,6 @@ -package com.pinterest.doctorkafka.tools; +package com.pinterest.doctork.tools; -import com.pinterest.doctorkafka.util.OperatorUtil; +import com.pinterest.doctork.util.OperatorUtil; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/tools/ReplicaStatsRetriever.java b/doctork/src/main/java/com/pinterest/doctork/tools/ReplicaStatsRetriever.java similarity index 93% rename from drkafka/src/main/java/com/pinterest/doctorkafka/tools/ReplicaStatsRetriever.java rename to doctork/src/main/java/com/pinterest/doctork/tools/ReplicaStatsRetriever.java index dfa8c781..43edf62c 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/tools/ReplicaStatsRetriever.java +++ b/doctork/src/main/java/com/pinterest/doctork/tools/ReplicaStatsRetriever.java @@ -1,9 +1,9 @@ -package com.pinterest.doctorkafka.tools; +package com.pinterest.doctork.tools; -import com.pinterest.doctorkafka.KafkaCluster; -import com.pinterest.doctorkafka.config.DoctorKafkaConfig; -import com.pinterest.doctorkafka.replicastats.ReplicaStatsManager; -import com.pinterest.doctorkafka.util.KafkaUtils; +import com.pinterest.doctork.KafkaCluster; +import com.pinterest.doctork.config.DoctorKConfig; +import com.pinterest.doctork.replicastats.ReplicaStatsManager; +import com.pinterest.doctork.util.KafkaUtils; import com.codahale.metrics.Histogram; import org.apache.commons.cli.CommandLine; @@ -80,7 +80,7 @@ public static void main(String[] args) throws Exception { long seconds = Long.parseLong(commandLine.getOptionValue(SECONDS)); long startTime = System.currentTimeMillis(); - ReplicaStatsManager replicaStatsManager = new ReplicaStatsManager(new DoctorKafkaConfig(configFilePath)); + ReplicaStatsManager replicaStatsManager = new ReplicaStatsManager(new DoctorKConfig(configFilePath)); replicaStatsManager.readPastReplicaStats(brokerStatsZk, SecurityProtocol.PLAINTEXT, brokerStatsTopic, seconds); long endTime = System.currentTimeMillis(); diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/tools/URPChecker.java b/doctork/src/main/java/com/pinterest/doctork/tools/URPChecker.java similarity index 95% rename from drkafka/src/main/java/com/pinterest/doctorkafka/tools/URPChecker.java rename to doctork/src/main/java/com/pinterest/doctork/tools/URPChecker.java index a928c92a..844e4d0a 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/tools/URPChecker.java +++ b/doctork/src/main/java/com/pinterest/doctork/tools/URPChecker.java @@ -1,8 +1,8 @@ -package com.pinterest.doctorkafka.tools; +package com.pinterest.doctork.tools; -import com.pinterest.doctorkafka.KafkaClusterManager; -import com.pinterest.doctorkafka.util.KafkaUtils; +import com.pinterest.doctork.KafkaClusterManager; +import com.pinterest.doctork.util.KafkaUtils; import kafka.utils.ZkUtils; import org.apache.commons.cli.CommandLine; diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/util/ApiUtils.java b/doctork/src/main/java/com/pinterest/doctork/util/ApiUtils.java similarity index 88% rename from drkafka/src/main/java/com/pinterest/doctorkafka/util/ApiUtils.java rename to doctork/src/main/java/com/pinterest/doctork/util/ApiUtils.java index 8b2e5b3d..21499127 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/util/ApiUtils.java +++ b/doctork/src/main/java/com/pinterest/doctork/util/ApiUtils.java @@ -1,4 +1,4 @@ -package com.pinterest.doctorkafka.util; +package com.pinterest.doctork.util; import org.apache.logging.log4j.Logger; diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/util/BrokerReplacer.java b/doctork/src/main/java/com/pinterest/doctork/util/BrokerReplacer.java similarity index 95% rename from drkafka/src/main/java/com/pinterest/doctorkafka/util/BrokerReplacer.java rename to doctork/src/main/java/com/pinterest/doctork/util/BrokerReplacer.java index 5fda3745..1060946d 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/util/BrokerReplacer.java +++ b/doctork/src/main/java/com/pinterest/doctork/util/BrokerReplacer.java @@ -1,6 +1,6 @@ -package com.pinterest.doctorkafka.util; +package com.pinterest.doctork.util; -import com.pinterest.doctorkafka.KafkaClusterManager; +import com.pinterest.doctork.KafkaClusterManager; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/util/OutOfSyncReplica.java b/doctork/src/main/java/com/pinterest/doctork/util/OutOfSyncReplica.java similarity index 98% rename from drkafka/src/main/java/com/pinterest/doctorkafka/util/OutOfSyncReplica.java rename to doctork/src/main/java/com/pinterest/doctork/util/OutOfSyncReplica.java index 75ee312f..a88c220d 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/util/OutOfSyncReplica.java +++ b/doctork/src/main/java/com/pinterest/doctork/util/OutOfSyncReplica.java @@ -1,4 +1,4 @@ -package com.pinterest.doctorkafka.util; +package com.pinterest.doctork.util; import org.apache.kafka.common.Node; import org.apache.kafka.common.PartitionInfo; diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/util/PreferredReplicaElectionInfo.java b/doctork/src/main/java/com/pinterest/doctork/util/PreferredReplicaElectionInfo.java similarity index 91% rename from drkafka/src/main/java/com/pinterest/doctorkafka/util/PreferredReplicaElectionInfo.java rename to doctork/src/main/java/com/pinterest/doctork/util/PreferredReplicaElectionInfo.java index f89bf7c7..bfc30e85 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/util/PreferredReplicaElectionInfo.java +++ b/doctork/src/main/java/com/pinterest/doctork/util/PreferredReplicaElectionInfo.java @@ -1,4 +1,4 @@ -package com.pinterest.doctorkafka.util; +package com.pinterest.doctork.util; import org.apache.kafka.common.TopicPartition; diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/util/ReassignmentInfo.java b/doctork/src/main/java/com/pinterest/doctork/util/ReassignmentInfo.java similarity index 86% rename from drkafka/src/main/java/com/pinterest/doctorkafka/util/ReassignmentInfo.java rename to doctork/src/main/java/com/pinterest/doctork/util/ReassignmentInfo.java index 4d5a32f5..d7a5584e 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/util/ReassignmentInfo.java +++ b/doctork/src/main/java/com/pinterest/doctork/util/ReassignmentInfo.java @@ -1,6 +1,6 @@ -package com.pinterest.doctorkafka.util; +package com.pinterest.doctork.util; -import com.pinterest.doctorkafka.KafkaBroker; +import com.pinterest.doctork.KafkaBroker; import org.apache.kafka.common.TopicPartition; diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/util/ReplicaStatsUtil.java b/doctork/src/main/java/com/pinterest/doctork/util/ReplicaStatsUtil.java similarity index 96% rename from drkafka/src/main/java/com/pinterest/doctorkafka/util/ReplicaStatsUtil.java rename to doctork/src/main/java/com/pinterest/doctork/util/ReplicaStatsUtil.java index b79b7b84..1b9b6a3b 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/util/ReplicaStatsUtil.java +++ b/doctork/src/main/java/com/pinterest/doctork/util/ReplicaStatsUtil.java @@ -1,4 +1,4 @@ -package com.pinterest.doctorkafka.util; +package com.pinterest.doctork.util; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.clients.consumer.OffsetAndTimestamp; diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/util/UnderReplicatedReason.java b/doctork/src/main/java/com/pinterest/doctork/util/UnderReplicatedReason.java similarity index 82% rename from drkafka/src/main/java/com/pinterest/doctorkafka/util/UnderReplicatedReason.java rename to doctork/src/main/java/com/pinterest/doctork/util/UnderReplicatedReason.java index 0440502a..8462b775 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/util/UnderReplicatedReason.java +++ b/doctork/src/main/java/com/pinterest/doctork/util/UnderReplicatedReason.java @@ -1,4 +1,4 @@ -package com.pinterest.doctorkafka.util; +package com.pinterest.doctork.util; public enum UnderReplicatedReason { FOLLOWER_FAILURE, diff --git a/drkafka/src/main/java/com/pinterest/doctorkafka/util/ZookeeperClient.java b/doctork/src/main/java/com/pinterest/doctork/util/ZookeeperClient.java similarity index 94% rename from drkafka/src/main/java/com/pinterest/doctorkafka/util/ZookeeperClient.java rename to doctork/src/main/java/com/pinterest/doctork/util/ZookeeperClient.java index 5c1dba2a..9f0464d5 100644 --- a/drkafka/src/main/java/com/pinterest/doctorkafka/util/ZookeeperClient.java +++ b/doctork/src/main/java/com/pinterest/doctork/util/ZookeeperClient.java @@ -1,4 +1,4 @@ -package com.pinterest.doctorkafka.util; +package com.pinterest.doctork.util; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -16,14 +16,14 @@ import java.util.List; /** - * DoctorKafka uses zookeeper to keep metadata - * zkurl/doctorkafka/clusters/${clustername}/alerts + * DoctorK uses zookeeper to keep metadata + * zkurl/doctork/clusters/${clustername}/alerts * /actions */ public class ZookeeperClient implements Closeable { private static final Logger LOG = LoggerFactory.getLogger(ZookeeperClient.class); - private static final String DOCTORKAFKA_PREFIX = "/doctorkafka"; + private static final String DOCTORK_PREFIX = "/doctork"; private static final int MAX_RETRIES = 5; private static final long RETRY_INTERVAL_MS = 100L; @@ -114,7 +114,7 @@ public List getChildren(String path) throws Exception { private String getBrokerReplacementPath(String clusterName) { - String path = DOCTORKAFKA_PREFIX + "/" + clusterName + "/actions/broker_replacement"; + String path = DOCTORK_PREFIX + "/" + clusterName + "/actions/broker_replacement"; return path; } diff --git a/drkafka/src/main/resources/log4j.properties b/doctork/src/main/resources/log4j.properties similarity index 100% rename from drkafka/src/main/resources/log4j.properties rename to doctork/src/main/resources/log4j.properties diff --git a/doctork/src/main/resources/versioning.properties b/doctork/src/main/resources/versioning.properties new file mode 100644 index 00000000..7ffb0884 --- /dev/null +++ b/doctork/src/main/resources/versioning.properties @@ -0,0 +1,2 @@ +version=0.2.4.10 +artifactId=doctork diff --git a/drkafka/src/main/resources/webapp/pages/clusterinfo.html b/doctork/src/main/resources/webapp/pages/clusterinfo.html similarity index 91% rename from drkafka/src/main/resources/webapp/pages/clusterinfo.html rename to doctork/src/main/resources/webapp/pages/clusterinfo.html index ae5ecb5e..8e1d42e1 100644 --- a/drkafka/src/main/resources/webapp/pages/clusterinfo.html +++ b/doctork/src/main/resources/webapp/pages/clusterinfo.html @@ -15,7 +15,7 @@ crossorigin="anonymous"> - DoctorKafka Cluster Info + DoctorK Cluster Info @@ -38,7 +38,7 @@

- DoctorKafka

+ DoctorK @@ -64,7 +64,7 @@

}, 15000) function ajaxFetchResult() { - $("#drkafkaSummary").html(" Retrieving doctorkafka actions ... "); + $("#doctorkSummary").html(" Retrieving doctork actions ... "); var the_url = "servlet/info"; $.ajax({ @@ -79,7 +79,7 @@

}, success: function (result, textStatus, jqXHR) { - $("#drkafkaSummary").html(result); + $("#doctorkSummary").html(result); } }); } diff --git a/drkafka/src/main/resources/webapp/pages/index.html b/doctork/src/main/resources/webapp/pages/index.html similarity index 88% rename from drkafka/src/main/resources/webapp/pages/index.html rename to doctork/src/main/resources/webapp/pages/index.html index 23511655..483fd0b0 100644 --- a/drkafka/src/main/resources/webapp/pages/index.html +++ b/doctork/src/main/resources/webapp/pages/index.html @@ -15,7 +15,7 @@ crossorigin="anonymous"> - DoctorKafka Latest Actions + DoctorK Latest Actions @@ -40,14 +40,14 @@

- DoctorKafka

+ DoctorK

Summary:

-
+
@@ -55,7 +55,7 @@

Summary:

- DoctorKafka Actions + DoctorK Actions

@@ -69,11 +69,11 @@

Summary:

ClusterName Size Under-replicated Partitions